@vitrine-kit/vitrine 0.3.1 → 0.4.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 (121) hide show
  1. package/dist/index.js +102 -83
  2. package/kit/registry/README.md +9 -0
  3. package/kit/registry/_index.json +74 -0
  4. package/kit/registry/cart/docs/cart.md +13 -0
  5. package/kit/registry/cart/feature.json +51 -0
  6. package/kit/registry/cart/files/app/(frontend)/cart/page.tsx +21 -0
  7. package/kit/registry/cart/files/app/api/cart/route.ts +41 -0
  8. package/kit/registry/cart/files/components/cart/AddToCart.tsx +40 -0
  9. package/kit/registry/cart/files/components/cart/CartIndicator.tsx +11 -0
  10. package/kit/registry/cart/files/components/cart/CartLineItem.tsx +27 -0
  11. package/kit/registry/cart/files/components/cart/CartSummary.tsx +31 -0
  12. package/kit/registry/cart/files/components/cart/CartView.tsx +31 -0
  13. package/kit/registry/cart/files/lib/cart/data.ts +12 -0
  14. package/kit/registry/cart/files/lib/cart/register.ts +10 -0
  15. package/kit/registry/catalog/docs/catalog.md +18 -0
  16. package/kit/registry/catalog/feature.json +43 -0
  17. package/kit/registry/catalog/files/components/catalog/CategoryNav.tsx +26 -0
  18. package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +42 -0
  19. package/kit/registry/catalog/files/components/catalog/ProductGrid.tsx +25 -0
  20. package/kit/registry/catalog/files/lib/catalog/data.ts +24 -0
  21. package/kit/registry/catalog/files/lib/catalog/register.ts +8 -0
  22. package/kit/registry/checkout/docs/checkout.md +17 -0
  23. package/kit/registry/checkout/feature.json +47 -0
  24. package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -0
  25. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +31 -0
  26. package/kit/registry/checkout/files/lib/checkout/fulfill.ts +72 -0
  27. package/kit/registry/checkout/files/lib/checkout/register.ts +8 -0
  28. package/kit/registry/checkout-paddle/docs/checkout-paddle.md +22 -0
  29. package/kit/registry/checkout-paddle/feature.json +62 -0
  30. package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +22 -0
  31. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +59 -0
  32. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/register.ts +7 -0
  33. package/kit/registry/checkout-stripe/docs/checkout-stripe.md +17 -0
  34. package/kit/registry/checkout-stripe/feature.json +54 -0
  35. package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +23 -0
  36. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +73 -0
  37. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/register.ts +7 -0
  38. package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +20 -0
  39. package/kit/registry/checkout-yookassa/feature.json +52 -0
  40. package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +22 -0
  41. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +82 -0
  42. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/register.ts +7 -0
  43. package/kit/registry/product-page/docs/product-page.md +16 -0
  44. package/kit/registry/product-page/feature.json +39 -0
  45. package/kit/registry/product-page/files/components/product/ProductGallery.tsx +37 -0
  46. package/kit/registry/product-page/files/components/product/ProductView.tsx +42 -0
  47. package/kit/registry/product-page/files/lib/product/data.ts +10 -0
  48. package/kit/registry/seo/docs/seo.md +14 -0
  49. package/kit/registry/seo/feature.json +35 -0
  50. package/kit/registry/seo/files/components/seo/JsonLd.tsx +15 -0
  51. package/kit/registry/seo/files/lib/seo/jsonld.ts +29 -0
  52. package/kit/registry/seo/files/lib/seo/metadata.ts +35 -0
  53. package/kit/registry/stubs/route-glue.d.ts +45 -0
  54. package/kit/registry/tsconfig.json +21 -0
  55. package/kit/registry/tsconfig.routes.json +9 -0
  56. package/kit/templates/README.md +27 -0
  57. package/kit/templates/backend-payload/files/.dockerignore +8 -0
  58. package/kit/templates/backend-payload/files/Dockerfile +29 -0
  59. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/not-found.tsx +18 -0
  60. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/page.tsx +18 -0
  61. package/kit/templates/backend-payload/files/app/(payload)/admin/importMap.js +3 -0
  62. package/kit/templates/backend-payload/files/app/(payload)/api/[...slug]/route.ts +18 -0
  63. package/kit/templates/backend-payload/files/app/(payload)/api/graphql/route.ts +6 -0
  64. package/kit/templates/backend-payload/files/app/(payload)/api/graphql-playground/route.ts +5 -0
  65. package/kit/templates/backend-payload/files/app/(payload)/custom.scss +1 -0
  66. package/kit/templates/backend-payload/files/app/(payload)/layout.tsx +23 -0
  67. package/kit/templates/backend-payload/files/docker-compose.yml +35 -0
  68. package/kit/templates/backend-payload/files/lib/adapter/db-decision.ts +43 -0
  69. package/kit/templates/backend-payload/files/lib/adapter/db-ping.ts +29 -0
  70. package/kit/templates/backend-payload/files/lib/adapter/db.ts +27 -0
  71. package/kit/templates/backend-payload/files/lib/adapter/index.ts +31 -0
  72. package/kit/templates/backend-payload/files/lib/adapter/map.ts +112 -0
  73. package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +79 -0
  74. package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +154 -0
  75. package/kit/templates/backend-payload/files/lib/adapter/payload-types.ts +50 -0
  76. package/kit/templates/backend-payload/files/lib/seed/admin.ts +23 -0
  77. package/kit/templates/backend-payload/files/lib/seed/demo.ts +79 -0
  78. package/kit/templates/backend-payload/files/lib/seed/guards.ts +5 -0
  79. package/kit/templates/backend-payload/files/lib/seed/richtext.ts +26 -0
  80. package/kit/templates/backend-payload/files/lib/seed/run.ts +51 -0
  81. package/kit/templates/backend-payload/files/next.config.mjs +10 -0
  82. package/kit/templates/backend-payload/files/payload.config.ts +32 -0
  83. package/kit/templates/backend-payload/files/seed-assets/placeholder-1.svg +5 -0
  84. package/kit/templates/backend-payload/files/seed-assets/placeholder-2.svg +5 -0
  85. package/kit/templates/backend-payload/files/seed-assets/placeholder-3.svg +5 -0
  86. package/kit/templates/backend-payload/files/seed-assets/placeholder-4.svg +5 -0
  87. package/kit/templates/backend-payload/files/seed-assets/placeholder-5.svg +5 -0
  88. package/kit/templates/backend-vendure/files/.dockerignore +8 -0
  89. package/kit/templates/backend-vendure/files/Dockerfile +24 -0
  90. package/kit/templates/backend-vendure/files/docker-compose.yml +50 -0
  91. package/kit/templates/backend-vendure/files/lib/adapter/graphql.ts +27 -0
  92. package/kit/templates/backend-vendure/files/lib/adapter/index.ts +20 -0
  93. package/kit/templates/backend-vendure/files/lib/adapter/map.ts +107 -0
  94. package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +62 -0
  95. package/kit/templates/backend-vendure/files/lib/adapter/vendure-commerce.ts +76 -0
  96. package/kit/templates/backend-vendure/files/lib/adapter/vendure-types.ts +61 -0
  97. package/kit/templates/backend-vendure/files/lib/seed/populate.ts +22 -0
  98. package/kit/templates/backend-vendure/files/src/index.ts +15 -0
  99. package/kit/templates/backend-vendure/files/vendure-config.ts +56 -0
  100. package/kit/templates/base/files/.claude/commands/add-feature.md +18 -0
  101. package/kit/templates/base/files/.claude/commands/design.md +14 -0
  102. package/kit/templates/base/files/.claude/commands/doctor.md +15 -0
  103. package/kit/templates/base/files/.claude/commands/setup.md +15 -0
  104. package/kit/templates/base/files/.claude/commands/update.md +16 -0
  105. package/kit/templates/base/files/AGENTS.md +20 -0
  106. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +28 -0
  107. package/kit/templates/base/files/app/(frontend)/layout.tsx +32 -0
  108. package/kit/templates/base/files/app/(frontend)/page.tsx +27 -0
  109. package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +43 -0
  110. package/kit/templates/base/files/app/globals.css +6 -0
  111. package/kit/templates/base/files/components/layout/SiteFooter.tsx +15 -0
  112. package/kit/templates/base/files/components/layout/SiteHeader.tsx +23 -0
  113. package/kit/templates/base/files/design/README.md +9 -0
  114. package/kit/templates/base/files/lib/site.ts +10 -0
  115. package/kit/templates/base/files/next.config.mjs +7 -0
  116. package/kit/templates/base/files/postcss.config.mjs +7 -0
  117. package/kit/templates/base/files/public/.gitkeep +0 -0
  118. package/kit/templates/base/files/tailwind.config.ts +13 -0
  119. package/kit/templates/base/files/tsconfig.json +25 -0
  120. package/kit/templates/tsconfig.json +18 -0
  121. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -121,25 +121,36 @@ function loadProject(root) {
121
121
  }
122
122
 
123
123
  // src/registry.ts
124
- import { existsSync as existsSync4 } from "fs";
125
- import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
124
+ import { existsSync as existsSync5 } from "fs";
125
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "path";
126
126
  import { featureManifestSchema } from "@vitrine-kit/contracts";
127
127
 
128
+ // src/bundled-kit.ts
129
+ import { existsSync as existsSync3 } from "fs";
130
+ import { dirname as dirname3, join as join3 } from "path";
131
+ import { fileURLToPath } from "url";
132
+ function bundledKitRoot() {
133
+ const distDir = dirname3(fileURLToPath(import.meta.url));
134
+ const candidate = join3(distDir, "..", "kit");
135
+ if (existsSync3(join3(candidate, "registry", "_index.json"))) return candidate;
136
+ return null;
137
+ }
138
+
128
139
  // src/cache.ts
129
- import { cpSync, existsSync as existsSync3, rmSync } from "fs";
130
- import { join as join3, resolve as resolve3 } from "path";
140
+ import { cpSync, existsSync as existsSync4, rmSync } from "fs";
141
+ import { join as join4, resolve as resolve3 } from "path";
131
142
  function vitrineHome() {
132
143
  if (process.env.VITRINE_HOME) return resolve3(process.env.VITRINE_HOME);
133
144
  const home = process.env.USERPROFILE ?? process.env.HOME;
134
145
  if (!home) throw new Error("[vitrine] could not determine the home directory (HOME/USERPROFILE)");
135
- return join3(home, ".vitrine");
146
+ return join4(home, ".vitrine");
136
147
  }
137
148
  function cachePaths(home = vitrineHome()) {
138
149
  return {
139
150
  root: home,
140
- registry: join3(home, "registry"),
141
- templates: join3(home, "templates"),
142
- meta: join3(home, "kit.json")
151
+ registry: join4(home, "registry"),
152
+ templates: join4(home, "templates"),
153
+ meta: join4(home, "kit.json")
143
154
  };
144
155
  }
145
156
  function readKitMeta(home = vitrineHome()) {
@@ -151,7 +162,7 @@ function writeKitMeta(home, meta) {
151
162
  `);
152
163
  }
153
164
  function readIndex(registryRoot) {
154
- const file = join3(registryRoot, "_index.json");
165
+ const file = join4(registryRoot, "_index.json");
155
166
  return exists(file) ? readJson(file) : null;
156
167
  }
157
168
  function computeChangelog(oldIndex, newIndex) {
@@ -180,16 +191,16 @@ function formatChangelog(entries) {
180
191
  }
181
192
  function populateCache(fromDir, opts = {}) {
182
193
  const home = opts.home ?? vitrineHome();
183
- const srcRegistry = join3(fromDir, "registry");
184
- const srcTemplates = join3(fromDir, "templates");
185
- if (!existsSync3(join3(srcRegistry, "_index.json"))) {
194
+ const srcRegistry = join4(fromDir, "registry");
195
+ const srcTemplates = join4(fromDir, "templates");
196
+ if (!existsSync4(join4(srcRegistry, "_index.json"))) {
186
197
  throw new Error(`[vitrine] "${fromDir}" has no registry/_index.json \u2014 not a kit tree`);
187
198
  }
188
199
  const paths = cachePaths(home);
189
200
  const oldIndex = readIndex(paths.registry);
190
201
  rmSync(paths.registry, { recursive: true, force: true });
191
202
  cpSync(srcRegistry, paths.registry, { recursive: true });
192
- if (existsSync3(srcTemplates)) {
203
+ if (existsSync4(srcTemplates)) {
193
204
  rmSync(paths.templates, { recursive: true, force: true });
194
205
  cpSync(srcTemplates, paths.templates, { recursive: true });
195
206
  }
@@ -207,9 +218,9 @@ function populateCache(fromDir, opts = {}) {
207
218
  function findUpRegistry(start) {
208
219
  let dir = resolve4(start);
209
220
  for (; ; ) {
210
- const candidate = join4(dir, "registry");
211
- if (existsSync4(join4(candidate, "_index.json"))) return candidate;
212
- const parent = dirname3(dir);
221
+ const candidate = join5(dir, "registry");
222
+ if (existsSync5(join5(candidate, "_index.json"))) return candidate;
223
+ const parent = dirname4(dir);
213
224
  if (parent === dir) return null;
214
225
  dir = parent;
215
226
  }
@@ -224,60 +235,62 @@ function resolveRegistryRoot(explicit) {
224
235
  home = null;
225
236
  }
226
237
  if (home) {
227
- const cache = join4(home, "registry");
228
- if (existsSync4(join4(cache, "_index.json"))) return cache;
238
+ const cache = join5(home, "registry");
239
+ if (existsSync5(join5(cache, "_index.json"))) return cache;
229
240
  }
230
241
  const dev = findUpRegistry(process.cwd());
231
242
  if (dev) return dev;
243
+ const bundled = bundledKitRoot();
244
+ if (bundled) return join5(bundled, "registry");
232
245
  throw new Error('[vitrine] registry not found. Run "vitrine kit update" or pass --registry.');
233
246
  }
234
247
  function createRegistrySource(explicitRoot) {
235
248
  const root = resolveRegistryRoot(explicitRoot);
236
249
  const cache = /* @__PURE__ */ new Map();
237
- const featureDir = (name) => join4(root, name);
238
- const hasFeature = (name) => existsSync4(join4(featureDir(name), "feature.json"));
250
+ const featureDir = (name) => join5(root, name);
251
+ const hasFeature = (name) => existsSync5(join5(featureDir(name), "feature.json"));
239
252
  const loadManifest = (name) => {
240
253
  const cached = cache.get(name);
241
254
  if (cached) return cached;
242
- const file = join4(featureDir(name), "feature.json");
243
- if (!existsSync4(file)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
255
+ const file = join5(featureDir(name), "feature.json");
256
+ if (!existsSync5(file)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
244
257
  const manifest = featureManifestSchema.parse(JSON.parse(readText(file)));
245
258
  cache.set(name, manifest);
246
259
  return manifest;
247
260
  };
248
261
  const listFeatures2 = () => {
249
- const index = JSON.parse(readText(join4(root, "_index.json")));
262
+ const index = JSON.parse(readText(join5(root, "_index.json")));
250
263
  return Object.keys(index.features ?? {});
251
264
  };
252
265
  return { root, featureDir, hasFeature, loadManifest, listFeatures: listFeatures2 };
253
266
  }
254
267
 
255
268
  // src/install.ts
256
- import { join as join6 } from "path";
269
+ import { join as join7 } from "path";
257
270
 
258
271
  // src/transaction.ts
259
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
260
- import { dirname as dirname4 } from "path";
272
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
273
+ import { dirname as dirname5 } from "path";
261
274
  var FsTransaction = class {
262
275
  backups = [];
263
276
  snapshot(path) {
264
277
  if (this.backups.some((b) => b.path === path)) return;
265
- if (existsSync5(path)) this.backups.push({ path, existed: true, prev: readFileSync2(path, "utf8") });
278
+ if (existsSync6(path)) this.backups.push({ path, existed: true, prev: readFileSync2(path, "utf8") });
266
279
  else this.backups.push({ path, existed: false });
267
280
  }
268
281
  write(path, content) {
269
282
  this.snapshot(path);
270
- mkdirSync2(dirname4(path), { recursive: true });
283
+ mkdirSync2(dirname5(path), { recursive: true });
271
284
  writeFileSync2(path, content, "utf8");
272
285
  }
273
286
  remove(path) {
274
287
  this.snapshot(path);
275
- if (existsSync5(path)) rmSync2(path);
288
+ if (existsSync6(path)) rmSync2(path);
276
289
  }
277
290
  rollback() {
278
291
  for (const b of [...this.backups].reverse()) {
279
292
  if (b.existed) writeFileSync2(b.path, b.prev, "utf8");
280
- else if (existsSync5(b.path)) rmSync2(b.path);
293
+ else if (existsSync6(b.path)) rmSync2(b.path);
281
294
  }
282
295
  this.backups = [];
283
296
  }
@@ -340,9 +353,11 @@ function renderSlotsFile(features) {
340
353
  const calls = withSlots.map((f) => ` register${pascalCase(f.name)}Slots();`);
341
354
  return [
342
355
  "// vitrine:generated \u2014 slot registration for installed features. Do not edit by hand.",
356
+ "import { slotRegistry } from '@vitrine-kit/core';",
343
357
  ...imports,
344
358
  "",
345
359
  "export function registerSlots(): void {",
360
+ " slotRegistry.clear();",
346
361
  ...calls.length ? calls : [" // no slot features"],
347
362
  "}",
348
363
  ""
@@ -357,9 +372,11 @@ function renderPaymentsFile(features) {
357
372
  const calls = withPayment.map((f) => ` register${pascalCase(f.name)}Provider();`);
358
373
  return [
359
374
  "// vitrine:generated \u2014 payment-provider registration for installed features. Do not edit by hand.",
375
+ "import { payments } from '@vitrine-kit/core';",
360
376
  ...imports,
361
377
  "",
362
378
  "export function registerPayments(): void {",
379
+ " payments.clear();",
363
380
  ...calls.length ? calls : [" // no payment features"],
364
381
  "}",
365
382
  ""
@@ -448,14 +465,14 @@ function renderNeutralTheme() {
448
465
  }
449
466
 
450
467
  // src/feature-files.ts
451
- import { join as join5 } from "path";
468
+ import { join as join6 } from "path";
452
469
  function* eachFeatureFile(featDir, map) {
453
- const src = join5(featDir, map.from);
470
+ const src = join6(featDir, map.from);
454
471
  if (!exists(src)) return;
455
472
  const rels = isDir(src) ? walkRelFiles(src) : [""];
456
473
  for (const rel of rels) {
457
- const srcAbs = rel ? join5(src, rel) : src;
458
- const repoRel = rel ? join5(map.to, rel) : map.to;
474
+ const srcAbs = rel ? join6(src, rel) : src;
475
+ const repoRel = rel ? join6(map.to, rel) : map.to;
459
476
  yield { srcAbs, repoRel, toRel: toPosix(repoRel) };
460
477
  }
461
478
  }
@@ -491,9 +508,9 @@ function validate(name, manifest, project) {
491
508
  }
492
509
  function copyFeatureFiles(project, name, manifest, registry, tx) {
493
510
  const featDir = registry.featureDir(name);
494
- const originalsBase = join6(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
511
+ const originalsBase = join7(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
495
512
  for (const map of manifest.files) {
496
- if (!exists(join6(featDir, map.from))) {
513
+ if (!exists(join7(featDir, map.from))) {
497
514
  throw new Error(`[vitrine] feature "${name}": no source "${map.from}"`);
498
515
  }
499
516
  for (const file of eachFeatureFile(featDir, map)) {
@@ -593,7 +610,7 @@ function removeFeature(project, name, registry) {
593
610
  throw new Error(`[vitrine] cannot remove "${name}": "${other}" depends on it`);
594
611
  }
595
612
  }
596
- const originalsDir = join6(projectPaths(project.root).originals, `${name}@${removed.version}`);
613
+ const originalsDir = join7(projectPaths(project.root).originals, `${name}@${removed.version}`);
597
614
  const featDir = registry.featureDir(name);
598
615
  const targets = /* @__PURE__ */ new Set();
599
616
  if (isDir(originalsDir)) for (const rel of walkRelFiles(originalsDir)) targets.add(rel);
@@ -616,21 +633,21 @@ function removeFeature(project, name, registry) {
616
633
 
617
634
  // src/design.ts
618
635
  import { spawnSync } from "child_process";
619
- import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
620
- import { delimiter, join as join7 } from "path";
636
+ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
637
+ import { delimiter, join as join8 } from "path";
621
638
  import { TOKEN_CSS_VARS as TOKEN_CSS_VARS2 } from "@vitrine-kit/contracts";
622
639
  var DESIGN_HEADING = "## INSTRUCTION: apply the design from /design";
623
640
  function findClaudeBin(explicit) {
624
641
  const pinned = explicit ?? process.env.VITRINE_CLAUDE_BIN;
625
642
  if (pinned) {
626
- if (existsSync6(pinned)) return pinned;
643
+ if (existsSync7(pinned)) return pinned;
627
644
  throw new Error(`[vitrine] Claude Code not found at path "${pinned}"`);
628
645
  }
629
646
  const names = process.platform === "win32" ? ["claude.cmd", "claude.exe", "claude.ps1", "claude"] : ["claude"];
630
647
  for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
631
648
  for (const name of names) {
632
- const full = join7(dir, name);
633
- if (existsSync6(full)) return full;
649
+ const full = join8(dir, name);
650
+ if (existsSync7(full)) return full;
634
651
  }
635
652
  }
636
653
  throw new Error(
@@ -638,8 +655,8 @@ function findClaudeBin(explicit) {
638
655
  );
639
656
  }
640
657
  function designHasInput(root) {
641
- const dir = join7(root, "design");
642
- if (!existsSync6(dir)) return false;
658
+ const dir = join8(root, "design");
659
+ if (!existsSync7(dir)) return false;
643
660
  return readdirSync2(dir).some((entry) => !/^readme\.md$/i.test(entry));
644
661
  }
645
662
  function extractDesignInstruction(claudeMd) {
@@ -649,7 +666,7 @@ function extractDesignInstruction(claudeMd) {
649
666
  return claudeMd.slice(start, next === -1 ? void 0 : next).trim();
650
667
  }
651
668
  function buildDesignPrompt(project) {
652
- const claudeMd = existsSync6(join7(project.root, "CLAUDE.md")) ? readText(join7(project.root, "CLAUDE.md")) : "";
669
+ const claudeMd = existsSync7(join8(project.root, "CLAUDE.md")) ? readText(join8(project.root, "CLAUDE.md")) : "";
653
670
  const instruction = extractDesignInstruction(claudeMd) ?? DESIGN_HEADING;
654
671
  const tokens = TOKEN_CSS_VARS2.map((v) => ` ${v}`).join("\n");
655
672
  return [
@@ -691,7 +708,7 @@ function designApply(project, opts = {}, runner = defaultRunner) {
691
708
  }
692
709
 
693
710
  // src/doctor.ts
694
- import { join as join8 } from "path";
711
+ import { join as join9 } from "path";
695
712
  function runDoctor(project, registry) {
696
713
  const paths = projectPaths(project.root);
697
714
  const issues = [];
@@ -733,7 +750,7 @@ function runDoctor(project, registry) {
733
750
  const featDir = registry.featureDir(name);
734
751
  for (const map of manifest.files) {
735
752
  for (const file of eachFeatureFile(featDir, map)) {
736
- if (!exists(join8(project.root, file.repoRel))) {
753
+ if (!exists(join9(project.root, file.repoRel))) {
737
754
  add({
738
755
  severity: "error",
739
756
  scope,
@@ -783,7 +800,7 @@ function runDoctor(project, registry) {
783
800
 
784
801
  // src/update.ts
785
802
  import { rmSync as rmSync3 } from "fs";
786
- import { join as join9 } from "path";
803
+ import { join as join10 } from "path";
787
804
 
788
805
  // src/merge.ts
789
806
  function splitLines(s) {
@@ -927,13 +944,13 @@ function planUpdate(project, name, registry) {
927
944
  const fromVersion = pin.version;
928
945
  const toVersion = manifest.kitVersion;
929
946
  const featDir = registry.featureDir(name);
930
- const originalsBase = join9(projectPaths(project.root).originals, `${name}@${fromVersion}`);
947
+ const originalsBase = join10(projectPaths(project.root).originals, `${name}@${fromVersion}`);
931
948
  const files = [];
932
949
  for (const map of manifest.files) {
933
950
  for (const file of eachFeatureFile(featDir, map)) {
934
951
  const theirs = readText(file.srcAbs);
935
- const oursPath = join9(project.root, file.repoRel);
936
- const basePath = join9(originalsBase, file.repoRel);
952
+ const oursPath = join10(project.root, file.repoRel);
953
+ const basePath = join10(originalsBase, file.repoRel);
937
954
  const ours = exists(oursPath) ? readText(oursPath) : null;
938
955
  const base = exists(basePath) ? readText(basePath) : null;
939
956
  if (ours === null) {
@@ -961,8 +978,8 @@ function applyUpdate(project, plan, registry) {
961
978
  const manifest = registry.loadManifest(plan.feature);
962
979
  const featDir = registry.featureDir(plan.feature);
963
980
  const paths = projectPaths(project.root);
964
- const newOriginals = join9(paths.originals, `${plan.feature}@${plan.toVersion}`);
965
- const oldOriginals = join9(paths.originals, `${plan.feature}@${plan.fromVersion}`);
981
+ const newOriginals = join10(paths.originals, `${plan.feature}@${plan.toVersion}`);
982
+ const oldOriginals = join10(paths.originals, `${plan.feature}@${plan.fromVersion}`);
966
983
  const tx = new FsTransaction();
967
984
  try {
968
985
  for (const f of plan.files) {
@@ -1037,32 +1054,34 @@ function diffFeatureCmd(name, registryRoot) {
1037
1054
 
1038
1055
  // src/init.ts
1039
1056
  import { readdirSync as readdirSync3 } from "fs";
1040
- import { join as join11 } from "path";
1057
+ import { join as join12 } from "path";
1041
1058
 
1042
- // src/kit.ts
1043
- var KIT_VERSION = "0.0.0";
1059
+ // src/kit-versions.generated.ts
1060
+ var KIT_VERSION = "0.4.0";
1044
1061
  var CONTRACTS_VERSION = "1.0.0";
1045
1062
  var CONTRACTS_RANGE = "^1.0.0";
1046
- var CORE_RANGE = "^0.1.0";
1047
- var BLUEPRINT_RANGE = "^0.1.0";
1063
+ var CORE_RANGE = "^0.2.0";
1064
+ var BLUEPRINT_RANGE = "^0.2.0";
1065
+
1066
+ // src/kit.ts
1048
1067
  var REACT_RANGE = "^18.3.1";
1049
1068
  var CLIENT_REACT_RANGE = "^19.0.0";
1050
1069
  var NEXT_RANGE = "^15.1.0";
1051
1070
  var PAYLOAD_RANGE = "^3.0.0";
1052
1071
 
1053
1072
  // src/templates.ts
1054
- import { existsSync as existsSync7 } from "fs";
1055
- import { dirname as dirname5, join as join10 } from "path";
1073
+ import { existsSync as existsSync8 } from "fs";
1074
+ import { dirname as dirname6, join as join11 } from "path";
1056
1075
  function templatesRoot(registryRoot) {
1057
- return join10(dirname5(registryRoot), "templates");
1076
+ return join11(dirname6(registryRoot), "templates");
1058
1077
  }
1059
1078
  function hasTemplate(root, name) {
1060
- return existsSync7(join10(root, name, "files"));
1079
+ return existsSync8(join11(root, name, "files"));
1061
1080
  }
1062
1081
  function copyTemplate(root, name, destRoot) {
1063
- const filesDir = join10(root, name, "files");
1082
+ const filesDir = join11(root, name, "files");
1064
1083
  const rels = walkRelFiles(filesDir);
1065
- for (const rel of rels) writeText(join10(destRoot, rel), readText(join10(filesDir, rel)));
1084
+ for (const rel of rels) writeText(join11(destRoot, rel), readText(join11(filesDir, rel)));
1066
1085
  return rels;
1067
1086
  }
1068
1087
 
@@ -1077,7 +1096,7 @@ var PAYMENT_PROVIDER_FEATURES = [
1077
1096
  ];
1078
1097
  function suggestFeatures(tier, registry, backend = defaultBackend(tier)) {
1079
1098
  const core = ["catalog", "product-page", "seo"];
1080
- const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe", "reviews"];
1099
+ const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe"];
1081
1100
  const desired = tier === "catalog" ? core : [...core, ...shop];
1082
1101
  return desired.filter((name) => registry.hasFeature(name));
1083
1102
  }
@@ -1254,8 +1273,8 @@ ${run}
1254
1273
 
1255
1274
  \`\`\`bash
1256
1275
  vitrine list # installed + available
1257
- vitrine add reviews # copy a feature: flag, slots, blueprint, env
1258
- vitrine remove reviews # remove (if the feature is removable)
1276
+ vitrine add cart # copy a feature: flag, slots, blueprint, env
1277
+ vitrine remove cart # remove (if the feature is removable)
1259
1278
  vitrine design apply # style the new feature
1260
1279
  \`\`\`
1261
1280
 
@@ -1294,10 +1313,10 @@ function scaffoldBase(opts) {
1294
1313
  const backendTemplate = `backend-${backend}`;
1295
1314
  if (hasTemplate(tRoot, backendTemplate)) copyTemplate(tRoot, backendTemplate, root);
1296
1315
  if (!baseCopied) {
1297
- writeText(join11(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1316
+ writeText(join12(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1298
1317
  }
1299
1318
  writeText(
1300
- join11(root, "vitrine.json"),
1319
+ join12(root, "vitrine.json"),
1301
1320
  `${JSON.stringify(
1302
1321
  { kitVersion: KIT_VERSION, contracts: CONTRACTS_VERSION, backend, tier, features: {} },
1303
1322
  null,
@@ -1306,7 +1325,7 @@ function scaffoldBase(opts) {
1306
1325
  `
1307
1326
  );
1308
1327
  writeText(
1309
- join11(root, "site.config.ts"),
1328
+ join12(root, "site.config.ts"),
1310
1329
  `import type { SiteConfig } from '@vitrine-kit/contracts';
1311
1330
 
1312
1331
  export const siteConfig: SiteConfig = {
@@ -1327,7 +1346,7 @@ export default siteConfig;
1327
1346
  `
1328
1347
  );
1329
1348
  writeText(
1330
- join11(root, "CLAUDE.md"),
1349
+ join12(root, "CLAUDE.md"),
1331
1350
  `# ${name}
1332
1351
 
1333
1352
  A Vitrine project. Backend: \`${backend}\`, tier: \`${tier}\`.
@@ -1393,12 +1412,12 @@ The step is idempotent: re-running converges and doesn't accumulate cruft.
1393
1412
  - **The user makes commits** \u2014 don't run \`git commit\`/\`git push\` without an explicit request.
1394
1413
  `
1395
1414
  );
1396
- writeText(join11(root, "README.md"), clientReadme(name, backend, tier));
1397
- writeText(join11(root, "lib", "slots.ts"), renderSlotsFile([]));
1398
- writeText(join11(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1399
- writeText(join11(root, "theme", "client.css"), renderNeutralTheme());
1400
- writeText(join11(root, ".env.example"), clientEnvExample(backend));
1401
- writeText(join11(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1415
+ writeText(join12(root, "README.md"), clientReadme(name, backend, tier));
1416
+ writeText(join12(root, "lib", "slots.ts"), renderSlotsFile([]));
1417
+ writeText(join12(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1418
+ writeText(join12(root, "theme", "client.css"), renderNeutralTheme());
1419
+ writeText(join12(root, ".env.example"), clientEnvExample(backend));
1420
+ writeText(join12(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1402
1421
  `);
1403
1422
  }
1404
1423
  function initProject(opts) {
@@ -1412,9 +1431,9 @@ function initProject(opts) {
1412
1431
 
1413
1432
  // src/kit-update.ts
1414
1433
  import { spawnSync as spawnSync2 } from "child_process";
1415
- import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1434
+ import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1416
1435
  import { tmpdir } from "os";
1417
- import { join as join12, resolve as resolve5 } from "path";
1436
+ import { join as join13, resolve as resolve5 } from "path";
1418
1437
  var REPO = "vitrine-kit/vitrine";
1419
1438
  function hasBin(bin) {
1420
1439
  const probe = process.platform === "win32" ? "where" : "which";
@@ -1427,7 +1446,7 @@ function acquireFromGh(version) {
1427
1446
  if (!hasBin("tar")) {
1428
1447
  throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
1429
1448
  }
1430
- const tmp = mkdtempSync(join12(tmpdir(), "vitrine-kit-"));
1449
+ const tmp = mkdtempSync(join13(tmpdir(), "vitrine-kit-"));
1431
1450
  const dl = spawnSync2(
1432
1451
  "gh",
1433
1452
  ["release", "download", ...version ? [version] : [], "--repo", REPO, "--archive=tar.gz", "--dir", tmp, "--clobber"],
@@ -1438,10 +1457,10 @@ function acquireFromGh(version) {
1438
1457
  }
1439
1458
  const tarball = readdirSync4(tmp).find((f) => f.endsWith(".tar.gz"));
1440
1459
  if (!tarball) throw new Error("[vitrine] release tarball not found after download");
1441
- if (spawnSync2("tar", ["-xzf", join12(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1460
+ if (spawnSync2("tar", ["-xzf", join13(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1442
1461
  throw new Error("[vitrine] tarball extraction failed");
1443
1462
  }
1444
- const root = readdirSync4(tmp).map((f) => join12(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync8(join12(p2, "registry", "_index.json")));
1463
+ const root = readdirSync4(tmp).map((f) => join13(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join13(p2, "registry", "_index.json")));
1445
1464
  if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
1446
1465
  return root;
1447
1466
  }
@@ -1451,7 +1470,7 @@ function kitUpdate(opts = {}) {
1451
1470
  }
1452
1471
  function kitStatus(home = vitrineHome()) {
1453
1472
  const meta = readKitMeta(home);
1454
- const idxFile = join12(cachePaths(home).registry, "_index.json");
1473
+ const idxFile = join13(cachePaths(home).registry, "_index.json");
1455
1474
  const idx = exists(idxFile) ? readJson(idxFile) : null;
1456
1475
  return {
1457
1476
  cached: meta !== null,
@@ -0,0 +1,9 @@
1
+ # registry/ — copy-in feature registry
2
+
3
+ The source for `vitrine init` / `vitrine add`. Each feature = a folder with `feature.json` (manifest) + `files/` (sources). On install the files are **copied** into the client repository (the client owns the code).
4
+
5
+ `_index.json` is the registry manifest: the list of all features + the kit version.
6
+
7
+ **Rule (spec §4, §13):** the registry holds only what varies per client (UI, sections, wiring). No critical logic — that lives in `@vitrine-kit/core`. A new feature must depend **only on the contracts**.
8
+
9
+ **Typecheck in the monorepo:** `pnpm typecheck:registry` (components + lib) and `pnpm typecheck:registry-routes` (Next API route glue via `stubs/route-glue.d.ts`). Full Next/Payload pages are checked when the client is instantiated.
@@ -0,0 +1,74 @@
1
+ {
2
+ "$schema": "../schemas/registry-index.schema.json",
3
+ "kitVersion": "0.4.0",
4
+ "contracts": "1.0.0",
5
+ "features": {
6
+ "catalog": {
7
+ "title": "Product catalog",
8
+ "kitVersion": "0.4.0",
9
+ "tier": [
10
+ "catalog",
11
+ "simple-store",
12
+ "full-store"
13
+ ]
14
+ },
15
+ "product-page": {
16
+ "title": "Product page",
17
+ "kitVersion": "0.4.0",
18
+ "tier": [
19
+ "catalog",
20
+ "simple-store",
21
+ "full-store"
22
+ ]
23
+ },
24
+ "seo": {
25
+ "title": "SEO — metadata and JSON-LD",
26
+ "kitVersion": "0.4.0",
27
+ "tier": [
28
+ "catalog",
29
+ "simple-store",
30
+ "full-store"
31
+ ]
32
+ },
33
+ "cart": {
34
+ "title": "Cart",
35
+ "kitVersion": "0.4.0",
36
+ "tier": [
37
+ "simple-store",
38
+ "full-store"
39
+ ]
40
+ },
41
+ "checkout": {
42
+ "title": "Checkout",
43
+ "kitVersion": "0.4.0",
44
+ "tier": [
45
+ "simple-store",
46
+ "full-store"
47
+ ]
48
+ },
49
+ "checkout-stripe": {
50
+ "title": "Payment — Stripe",
51
+ "kitVersion": "0.4.0",
52
+ "tier": [
53
+ "simple-store",
54
+ "full-store"
55
+ ]
56
+ },
57
+ "checkout-paddle": {
58
+ "title": "Payment — Paddle",
59
+ "kitVersion": "0.4.0",
60
+ "tier": [
61
+ "simple-store",
62
+ "full-store"
63
+ ]
64
+ },
65
+ "checkout-yookassa": {
66
+ "title": "Payment — YooKassa",
67
+ "kitVersion": "0.4.0",
68
+ "tier": [
69
+ "simple-store",
70
+ "full-store"
71
+ ]
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,13 @@
1
+ # Feature: cart
2
+
3
+ The cart for the `simple-store` / `full-store` tiers. Depends only on the contracts:
4
+ data goes through `CommerceBackend`, the arithmetic lives in `@vitrine-kit/core` (critical logic).
5
+
6
+ - **Components:** `CartView`, `CartLineItem`, `CartSummary` (hosts the `cart.summary` slot),
7
+ `AddToCart` (client component, `product.purchase` slot), `CartIndicator` (`global.header-actions` slot).
8
+ - **Routes/API (Next glue):** `/cart` (page), `POST/PATCH/DELETE /api/cart`
9
+ (cart id in an httpOnly cookie, mutations via `CommerceBackend`).
10
+ - **Slots:** `product.purchase` → `AddToCart`; `global.header-actions` → `CartIndicator`;
11
+ hosts `cart.summary` (where checkout-stripe places the checkout button).
12
+
13
+ Styled with tokens; the AI design step doesn't touch behavior.
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "../../schemas/feature.schema.json",
3
+ "name": "cart",
4
+ "title": "Cart",
5
+ "kitVersion": "0.4.0",
6
+ "requiresContracts": ">=1.0.0 <2.0.0",
7
+ "tier": [
8
+ "simple-store",
9
+ "full-store"
10
+ ],
11
+ "registryDependencies": [],
12
+ "corePackages": {
13
+ "@vitrine-kit/core": ">=0.1.0"
14
+ },
15
+ "npm": [],
16
+ "files": [
17
+ {
18
+ "from": "files/components/cart/",
19
+ "to": "components/cart/"
20
+ },
21
+ {
22
+ "from": "files/lib/cart/",
23
+ "to": "lib/cart/"
24
+ },
25
+ {
26
+ "from": "files/app/",
27
+ "to": "app/"
28
+ }
29
+ ],
30
+ "config": {
31
+ "set": {
32
+ "features.cart": true
33
+ }
34
+ },
35
+ "slots": [
36
+ {
37
+ "slot": "product.purchase",
38
+ "component": "AddToCart",
39
+ "order": 10
40
+ },
41
+ {
42
+ "slot": "global.header-actions",
43
+ "component": "CartIndicator",
44
+ "order": 20
45
+ }
46
+ ],
47
+ "env": [],
48
+ "claudeDoc": "docs/cart.md",
49
+ "conflicts": [],
50
+ "removable": true
51
+ }
@@ -0,0 +1,21 @@
1
+ // Cart page. The cart id is stored in a cookie; data comes via the CommerceBackend
2
+ // contract (the backend adapter in lib/adapter). Not typechecked in the monorepo
3
+ // (Next glue), verified when the client is instantiated.
4
+ import { cookies } from 'next/headers';
5
+ import { emptyCart } from '@vitrine-kit/core';
6
+ import { getCommerceBackend } from '@/lib/adapter';
7
+ import { CartView } from '@/components/cart/CartView';
8
+ import { siteConfig } from '@/site.config';
9
+
10
+ export default async function CartPage() {
11
+ const cartId = (await cookies()).get('vitrine_cart')?.value;
12
+ const commerce = await getCommerceBackend();
13
+ const cart = cartId ? await commerce.getCart(cartId) : null;
14
+
15
+ return (
16
+ <section className="flex flex-col gap-section">
17
+ <h1 className="font-heading text-fg">Cart</h1>
18
+ <CartView cart={cart ?? emptyCart('', siteConfig.i18n.currency)} />
19
+ </section>
20
+ );
21
+ }