@vitrine-kit/vitrine 0.3.1 → 0.4.1

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 (122) hide show
  1. package/dist/index.js +313 -135
  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 +46 -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 +25 -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 +80 -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 +19 -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 +80 -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 +23 -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 +92 -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/.gitattributes +12 -0
  106. package/kit/templates/base/files/AGENTS.md +20 -0
  107. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +28 -0
  108. package/kit/templates/base/files/app/(frontend)/layout.tsx +32 -0
  109. package/kit/templates/base/files/app/(frontend)/page.tsx +27 -0
  110. package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +43 -0
  111. package/kit/templates/base/files/app/globals.css +6 -0
  112. package/kit/templates/base/files/components/layout/SiteFooter.tsx +15 -0
  113. package/kit/templates/base/files/components/layout/SiteHeader.tsx +23 -0
  114. package/kit/templates/base/files/design/README.md +9 -0
  115. package/kit/templates/base/files/lib/site.ts +10 -0
  116. package/kit/templates/base/files/next.config.mjs +7 -0
  117. package/kit/templates/base/files/postcss.config.mjs +7 -0
  118. package/kit/templates/base/files/public/.gitkeep +0 -0
  119. package/kit/templates/base/files/tailwind.config.ts +13 -0
  120. package/kit/templates/base/files/tsconfig.json +25 -0
  121. package/kit/templates/tsconfig.json +18 -0
  122. package/package.json +9 -5
package/dist/index.js CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync3 } from "fs";
5
- import { resolve as resolve6 } from "path";
5
+ import { resolve as resolve7 } from "path";
6
6
  import { Command } from "commander";
7
7
  import * as p from "@clack/prompts";
8
8
 
9
+ // src/commands.ts
10
+ import { join as join11, resolve as resolve5 } from "path";
11
+
9
12
  // src/project.ts
10
13
  import { existsSync as existsSync2 } from "fs";
11
14
  import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
@@ -34,9 +37,12 @@ function readText(p2) {
34
37
  function readJson(p2) {
35
38
  return JSON.parse(readText(p2));
36
39
  }
40
+ function normalizeEol(s) {
41
+ return s.includes("\r") ? s.replace(/\r\n/g, "\n") : s;
42
+ }
37
43
  function writeText(p2, content) {
38
44
  mkdirSync(dirname(p2), { recursive: true });
39
- writeFileSync(p2, content, "utf8");
45
+ writeFileSync(p2, normalizeEol(content), "utf8");
40
46
  }
41
47
  function toPosix(p2) {
42
48
  return p2.split("\\").join("/");
@@ -50,6 +56,16 @@ function safeJoin(root, ...segs) {
50
56
  }
51
57
  return target;
52
58
  }
59
+ function assertFeatureName(name) {
60
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
61
+ throw new Error(`[vitrine] invalid feature name "${name}"`);
62
+ }
63
+ }
64
+ function assertSafeRel(p2, what) {
65
+ if (isAbsolute(p2) || p2.split(/[\\/]/).some((seg) => seg === "..")) {
66
+ throw new Error(`[vitrine] ${what} "${p2}" must be a relative path without ".."`);
67
+ }
68
+ }
53
69
  function parseEnvKeys(text2) {
54
70
  return new Set(
55
71
  text2.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && l.includes("=")).map((l) => l.split("=")[0]?.trim() ?? "").filter(Boolean)
@@ -121,25 +137,37 @@ function loadProject(root) {
121
137
  }
122
138
 
123
139
  // src/registry.ts
124
- import { existsSync as existsSync4 } from "fs";
125
- import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
140
+ import { existsSync as existsSync5 } from "fs";
141
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "path";
126
142
  import { featureManifestSchema } from "@vitrine-kit/contracts";
127
143
 
144
+ // src/bundled-kit.ts
145
+ import { existsSync as existsSync3 } from "fs";
146
+ import { dirname as dirname3, join as join3 } from "path";
147
+ import { fileURLToPath } from "url";
148
+ function bundledKitRoot() {
149
+ const distDir = dirname3(fileURLToPath(import.meta.url));
150
+ const candidate = join3(distDir, "..", "kit");
151
+ if (existsSync3(join3(candidate, "registry", "_index.json"))) return candidate;
152
+ return null;
153
+ }
154
+
128
155
  // src/cache.ts
129
- import { cpSync, existsSync as existsSync3, rmSync } from "fs";
130
- import { join as join3, resolve as resolve3 } from "path";
156
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2, renameSync, rmSync } from "fs";
157
+ import { join as join4, resolve as resolve3 } from "path";
158
+ import { registryIndexSchema } from "@vitrine-kit/contracts";
131
159
  function vitrineHome() {
132
160
  if (process.env.VITRINE_HOME) return resolve3(process.env.VITRINE_HOME);
133
161
  const home = process.env.USERPROFILE ?? process.env.HOME;
134
162
  if (!home) throw new Error("[vitrine] could not determine the home directory (HOME/USERPROFILE)");
135
- return join3(home, ".vitrine");
163
+ return join4(home, ".vitrine");
136
164
  }
137
165
  function cachePaths(home = vitrineHome()) {
138
166
  return {
139
167
  root: home,
140
- registry: join3(home, "registry"),
141
- templates: join3(home, "templates"),
142
- meta: join3(home, "kit.json")
168
+ registry: join4(home, "registry"),
169
+ templates: join4(home, "templates"),
170
+ meta: join4(home, "kit.json")
143
171
  };
144
172
  }
145
173
  function readKitMeta(home = vitrineHome()) {
@@ -151,7 +179,7 @@ function writeKitMeta(home, meta) {
151
179
  `);
152
180
  }
153
181
  function readIndex(registryRoot) {
154
- const file = join3(registryRoot, "_index.json");
182
+ const file = join4(registryRoot, "_index.json");
155
183
  return exists(file) ? readJson(file) : null;
156
184
  }
157
185
  function computeChangelog(oldIndex, newIndex) {
@@ -180,18 +208,29 @@ function formatChangelog(entries) {
180
208
  }
181
209
  function populateCache(fromDir, opts = {}) {
182
210
  const home = opts.home ?? vitrineHome();
183
- const srcRegistry = join3(fromDir, "registry");
184
- const srcTemplates = join3(fromDir, "templates");
185
- if (!existsSync3(join3(srcRegistry, "_index.json"))) {
211
+ const srcRegistry = join4(fromDir, "registry");
212
+ const srcTemplates = join4(fromDir, "templates");
213
+ if (!existsSync4(join4(srcRegistry, "_index.json"))) {
186
214
  throw new Error(`[vitrine] "${fromDir}" has no registry/_index.json \u2014 not a kit tree`);
187
215
  }
188
216
  const paths = cachePaths(home);
189
217
  const oldIndex = readIndex(paths.registry);
190
- rmSync(paths.registry, { recursive: true, force: true });
191
- cpSync(srcRegistry, paths.registry, { recursive: true });
192
- if (existsSync3(srcTemplates)) {
193
- rmSync(paths.templates, { recursive: true, force: true });
194
- cpSync(srcTemplates, paths.templates, { recursive: true });
218
+ const staging = join4(home, `.staging-${process.pid}`);
219
+ try {
220
+ rmSync(staging, { recursive: true, force: true });
221
+ mkdirSync2(staging, { recursive: true });
222
+ cpSync(srcRegistry, join4(staging, "registry"), { recursive: true });
223
+ registryIndexSchema.parse(readJson(join4(staging, "registry", "_index.json")));
224
+ const hasTemplates = existsSync4(srcTemplates);
225
+ if (hasTemplates) cpSync(srcTemplates, join4(staging, "templates"), { recursive: true });
226
+ rmSync(paths.registry, { recursive: true, force: true });
227
+ renameSync(join4(staging, "registry"), paths.registry);
228
+ if (hasTemplates) {
229
+ rmSync(paths.templates, { recursive: true, force: true });
230
+ renameSync(join4(staging, "templates"), paths.templates);
231
+ }
232
+ } finally {
233
+ rmSync(staging, { recursive: true, force: true });
195
234
  }
196
235
  const newIndex = readIndex(paths.registry);
197
236
  const kitVersion = newIndex?.kitVersion ?? "0.0.0";
@@ -207,9 +246,9 @@ function populateCache(fromDir, opts = {}) {
207
246
  function findUpRegistry(start) {
208
247
  let dir = resolve4(start);
209
248
  for (; ; ) {
210
- const candidate = join4(dir, "registry");
211
- if (existsSync4(join4(candidate, "_index.json"))) return candidate;
212
- const parent = dirname3(dir);
249
+ const candidate = join5(dir, "registry");
250
+ if (existsSync5(join5(candidate, "_index.json"))) return candidate;
251
+ const parent = dirname4(dir);
213
252
  if (parent === dir) return null;
214
253
  dir = parent;
215
254
  }
@@ -224,60 +263,69 @@ function resolveRegistryRoot(explicit) {
224
263
  home = null;
225
264
  }
226
265
  if (home) {
227
- const cache = join4(home, "registry");
228
- if (existsSync4(join4(cache, "_index.json"))) return cache;
266
+ const cache = join5(home, "registry");
267
+ if (existsSync5(join5(cache, "_index.json"))) return cache;
229
268
  }
230
269
  const dev = findUpRegistry(process.cwd());
231
270
  if (dev) return dev;
271
+ const bundled = bundledKitRoot();
272
+ if (bundled) return join5(bundled, "registry");
232
273
  throw new Error('[vitrine] registry not found. Run "vitrine kit update" or pass --registry.');
233
274
  }
234
275
  function createRegistrySource(explicitRoot) {
235
276
  const root = resolveRegistryRoot(explicitRoot);
236
277
  const cache = /* @__PURE__ */ new Map();
237
- const featureDir = (name) => join4(root, name);
238
- const hasFeature = (name) => existsSync4(join4(featureDir(name), "feature.json"));
278
+ const featureDir = (name) => {
279
+ assertFeatureName(name);
280
+ return join5(root, name);
281
+ };
282
+ const hasFeature = (name) => existsSync5(join5(featureDir(name), "feature.json"));
239
283
  const loadManifest = (name) => {
240
284
  const cached = cache.get(name);
241
285
  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`);
286
+ const file = join5(featureDir(name), "feature.json");
287
+ if (!existsSync5(file)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
244
288
  const manifest = featureManifestSchema.parse(JSON.parse(readText(file)));
289
+ for (const map of manifest.files) {
290
+ assertSafeRel(map.from, `feature "${name}": files.from`);
291
+ assertSafeRel(map.to, `feature "${name}": files.to`);
292
+ }
245
293
  cache.set(name, manifest);
246
294
  return manifest;
247
295
  };
248
296
  const listFeatures2 = () => {
249
- const index = JSON.parse(readText(join4(root, "_index.json")));
297
+ const index = JSON.parse(readText(join5(root, "_index.json")));
250
298
  return Object.keys(index.features ?? {});
251
299
  };
252
300
  return { root, featureDir, hasFeature, loadManifest, listFeatures: listFeatures2 };
253
301
  }
254
302
 
255
303
  // src/install.ts
256
- import { join as join6 } from "path";
304
+ import { join as join7 } from "path";
257
305
 
258
306
  // 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";
307
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
308
+ import { dirname as dirname5 } from "path";
261
309
  var FsTransaction = class {
262
310
  backups = [];
263
311
  snapshot(path) {
264
312
  if (this.backups.some((b) => b.path === path)) return;
265
- if (existsSync5(path)) this.backups.push({ path, existed: true, prev: readFileSync2(path, "utf8") });
313
+ if (existsSync6(path)) this.backups.push({ path, existed: true, prev: readFileSync2(path, "utf8") });
266
314
  else this.backups.push({ path, existed: false });
267
315
  }
268
316
  write(path, content) {
269
317
  this.snapshot(path);
270
- mkdirSync2(dirname4(path), { recursive: true });
271
- writeFileSync2(path, content, "utf8");
318
+ mkdirSync3(dirname5(path), { recursive: true });
319
+ writeFileSync2(path, normalizeEol(content), "utf8");
272
320
  }
273
321
  remove(path) {
274
322
  this.snapshot(path);
275
- if (existsSync5(path)) rmSync2(path);
323
+ if (existsSync6(path)) rmSync2(path);
276
324
  }
277
325
  rollback() {
278
326
  for (const b of [...this.backups].reverse()) {
279
327
  if (b.existed) writeFileSync2(b.path, b.prev, "utf8");
280
- else if (existsSync5(b.path)) rmSync2(b.path);
328
+ else if (existsSync6(b.path)) rmSync2(b.path);
281
329
  }
282
330
  this.backups = [];
283
331
  }
@@ -340,9 +388,11 @@ function renderSlotsFile(features) {
340
388
  const calls = withSlots.map((f) => ` register${pascalCase(f.name)}Slots();`);
341
389
  return [
342
390
  "// vitrine:generated \u2014 slot registration for installed features. Do not edit by hand.",
391
+ "import { slotRegistry } from '@vitrine-kit/core';",
343
392
  ...imports,
344
393
  "",
345
394
  "export function registerSlots(): void {",
395
+ " slotRegistry.clear();",
346
396
  ...calls.length ? calls : [" // no slot features"],
347
397
  "}",
348
398
  ""
@@ -357,9 +407,11 @@ function renderPaymentsFile(features) {
357
407
  const calls = withPayment.map((f) => ` register${pascalCase(f.name)}Provider();`);
358
408
  return [
359
409
  "// vitrine:generated \u2014 payment-provider registration for installed features. Do not edit by hand.",
410
+ "import { payments } from '@vitrine-kit/core';",
360
411
  ...imports,
361
412
  "",
362
413
  "export function registerPayments(): void {",
414
+ " payments.clear();",
363
415
  ...calls.length ? calls : [" // no payment features"],
364
416
  "}",
365
417
  ""
@@ -448,14 +500,14 @@ function renderNeutralTheme() {
448
500
  }
449
501
 
450
502
  // src/feature-files.ts
451
- import { join as join5 } from "path";
503
+ import { join as join6 } from "path";
452
504
  function* eachFeatureFile(featDir, map) {
453
- const src = join5(featDir, map.from);
505
+ const src = safeJoin(featDir, map.from);
454
506
  if (!exists(src)) return;
455
507
  const rels = isDir(src) ? walkRelFiles(src) : [""];
456
508
  for (const rel of rels) {
457
- const srcAbs = rel ? join5(src, rel) : src;
458
- const repoRel = rel ? join5(map.to, rel) : map.to;
509
+ const srcAbs = rel ? join6(src, rel) : src;
510
+ const repoRel = rel ? join6(map.to, rel) : map.to;
459
511
  yield { srcAbs, repoRel, toRel: toPosix(repoRel) };
460
512
  }
461
513
  }
@@ -464,38 +516,41 @@ function* eachFeatureFile(featDir, map) {
464
516
  function resolveOrder(names, registry) {
465
517
  const order = [];
466
518
  const seen = /* @__PURE__ */ new Set();
519
+ const visiting = /* @__PURE__ */ new Set();
467
520
  const visit = (name) => {
468
521
  if (seen.has(name)) return;
522
+ if (visiting.has(name)) {
523
+ throw new Error(`[vitrine] circular registryDependencies involving "${name}"`);
524
+ }
469
525
  if (!registry.hasFeature(name)) {
470
526
  throw new Error(`[vitrine] feature "${name}" not found in the registry`);
471
527
  }
472
- seen.add(name);
528
+ visiting.add(name);
473
529
  const manifest = registry.loadManifest(name);
474
530
  for (const dep of manifest.registryDependencies ?? []) visit(dep);
531
+ visiting.delete(name);
532
+ seen.add(name);
475
533
  order.push(name);
476
534
  };
477
535
  for (const name of names) visit(name);
478
536
  return order;
479
537
  }
480
- function validate(name, manifest, project) {
538
+ function validate(name, manifest, project, present) {
481
539
  if (!manifest.tier.includes(project.lock.tier)) {
482
540
  throw new Error(
483
541
  `[vitrine] feature "${name}" does not support tier "${project.lock.tier}" (only ${manifest.tier.join(", ")})`
484
542
  );
485
543
  }
486
544
  for (const conflict of manifest.conflicts ?? []) {
487
- if (project.lock.features[conflict]) {
545
+ if (present.has(conflict)) {
488
546
  throw new Error(`[vitrine] conflict: "${name}" is incompatible with the installed "${conflict}"`);
489
547
  }
490
548
  }
491
549
  }
492
550
  function copyFeatureFiles(project, name, manifest, registry, tx) {
493
551
  const featDir = registry.featureDir(name);
494
- const originalsBase = join6(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
552
+ const originalsBase = join7(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
495
553
  for (const map of manifest.files) {
496
- if (!exists(join6(featDir, map.from))) {
497
- throw new Error(`[vitrine] feature "${name}": no source "${map.from}"`);
498
- }
499
554
  for (const file of eachFeatureFile(featDir, map)) {
500
555
  const content = readText(file.srcAbs);
501
556
  tx.write(safeJoin(project.root, file.repoRel), content);
@@ -557,11 +612,21 @@ function installFeatures(project, names, registry) {
557
612
  if (toInstall.length === 0) {
558
613
  return { installed: [], skipped: order };
559
614
  }
615
+ const present = new Set(installedNames);
616
+ for (const name of toInstall) {
617
+ const manifest = registry.loadManifest(name);
618
+ validate(name, manifest, project, present);
619
+ present.add(name);
620
+ for (const map of manifest.files) {
621
+ if (!exists(join7(registry.featureDir(name), map.from))) {
622
+ throw new Error(`[vitrine] feature "${name}": no source "${map.from}"`);
623
+ }
624
+ }
625
+ }
560
626
  const tx = new FsTransaction();
561
627
  try {
562
628
  for (const name of toInstall) {
563
629
  const manifest = registry.loadManifest(name);
564
- validate(name, manifest, project);
565
630
  copyFeatureFiles(project, name, manifest, registry, tx);
566
631
  project.lock.features[name] = { version: manifest.kitVersion };
567
632
  }
@@ -593,7 +658,7 @@ function removeFeature(project, name, registry) {
593
658
  throw new Error(`[vitrine] cannot remove "${name}": "${other}" depends on it`);
594
659
  }
595
660
  }
596
- const originalsDir = join6(projectPaths(project.root).originals, `${name}@${removed.version}`);
661
+ const originalsDir = join7(projectPaths(project.root).originals, `${name}@${removed.version}`);
597
662
  const featDir = registry.featureDir(name);
598
663
  const targets = /* @__PURE__ */ new Set();
599
664
  if (isDir(originalsDir)) for (const rel of walkRelFiles(originalsDir)) targets.add(rel);
@@ -606,6 +671,20 @@ function removeFeature(project, name, registry) {
606
671
  }
607
672
  delete project.lock.features[name];
608
673
  regenerateDerived(project, registry, tx);
674
+ const keptKeys = new Set(
675
+ Object.keys(project.lock.features).flatMap(
676
+ (other) => (registry.loadManifest(other).env ?? []).map((e) => e.key)
677
+ )
678
+ );
679
+ const stale = (manifest.env ?? []).map((e) => e.key).filter((k) => !keptKeys.has(k));
680
+ const envPath = projectPaths(project.root).env;
681
+ if (stale.length > 0 && exists(envPath)) {
682
+ const lines = readText(envPath).split("\n").filter((l) => {
683
+ const key = l.includes("=") ? l.split("=")[0]?.trim() : void 0;
684
+ return !(key && stale.includes(key));
685
+ });
686
+ tx.write(envPath, lines.join("\n"));
687
+ }
609
688
  tx.commit();
610
689
  } catch (error) {
611
690
  tx.rollback();
@@ -616,21 +695,22 @@ function removeFeature(project, name, registry) {
616
695
 
617
696
  // src/design.ts
618
697
  import { spawnSync } from "child_process";
619
- import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
620
- import { delimiter, join as join7 } from "path";
698
+ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
699
+ import { delimiter, join as join8 } from "path";
621
700
  import { TOKEN_CSS_VARS as TOKEN_CSS_VARS2 } from "@vitrine-kit/contracts";
622
701
  var DESIGN_HEADING = "## INSTRUCTION: apply the design from /design";
702
+ var MAX_INSTRUCTION_CHARS = 4e3;
623
703
  function findClaudeBin(explicit) {
624
704
  const pinned = explicit ?? process.env.VITRINE_CLAUDE_BIN;
625
705
  if (pinned) {
626
- if (existsSync6(pinned)) return pinned;
706
+ if (existsSync7(pinned)) return pinned;
627
707
  throw new Error(`[vitrine] Claude Code not found at path "${pinned}"`);
628
708
  }
629
709
  const names = process.platform === "win32" ? ["claude.cmd", "claude.exe", "claude.ps1", "claude"] : ["claude"];
630
710
  for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
631
711
  for (const name of names) {
632
- const full = join7(dir, name);
633
- if (existsSync6(full)) return full;
712
+ const full = join8(dir, name);
713
+ if (existsSync7(full)) return full;
634
714
  }
635
715
  }
636
716
  throw new Error(
@@ -638,8 +718,8 @@ function findClaudeBin(explicit) {
638
718
  );
639
719
  }
640
720
  function designHasInput(root) {
641
- const dir = join7(root, "design");
642
- if (!existsSync6(dir)) return false;
721
+ const dir = join8(root, "design");
722
+ if (!existsSync7(dir)) return false;
643
723
  return readdirSync2(dir).some((entry) => !/^readme\.md$/i.test(entry));
644
724
  }
645
725
  function extractDesignInstruction(claudeMd) {
@@ -649,8 +729,12 @@ function extractDesignInstruction(claudeMd) {
649
729
  return claudeMd.slice(start, next === -1 ? void 0 : next).trim();
650
730
  }
651
731
  function buildDesignPrompt(project) {
652
- const claudeMd = existsSync6(join7(project.root, "CLAUDE.md")) ? readText(join7(project.root, "CLAUDE.md")) : "";
653
- const instruction = extractDesignInstruction(claudeMd) ?? DESIGN_HEADING;
732
+ const claudeMd = existsSync7(join8(project.root, "CLAUDE.md")) ? readText(join8(project.root, "CLAUDE.md")) : "";
733
+ let instruction = extractDesignInstruction(claudeMd) ?? DESIGN_HEADING;
734
+ if (instruction.length > MAX_INSTRUCTION_CHARS) {
735
+ instruction = `${instruction.slice(0, MAX_INSTRUCTION_CHARS)}
736
+ [\u2026truncated by vitrine: the design instruction exceeds ${MAX_INSTRUCTION_CHARS} chars]`;
737
+ }
654
738
  const tokens = TOKEN_CSS_VARS2.map((v) => ` ${v}`).join("\n");
655
739
  return [
656
740
  instruction,
@@ -670,6 +754,19 @@ var defaultRunner = ({ bin, args, cwd }) => {
670
754
  if (res.error) throw res.error;
671
755
  return res.status ?? 0;
672
756
  };
757
+ function preflightClaude(bin) {
758
+ const res = spawnSync(bin, ["--version"], {
759
+ stdio: "ignore",
760
+ // .cmd/.bat shims cannot be spawned directly on modern Node — safe here since
761
+ // the args carry no user input.
762
+ shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(bin)
763
+ });
764
+ if (res.error || res.status !== 0) {
765
+ throw new Error(
766
+ `[vitrine] "${bin}" did not answer --version \u2014 Claude Code missing or broken? Reinstall it (npm i -g @anthropic-ai/claude-code) or pass another --bin.`
767
+ );
768
+ }
769
+ }
673
770
  function planDesignApply(project, opts = {}) {
674
771
  const bin = findClaudeBin(opts.bin);
675
772
  const prompt = buildDesignPrompt(project);
@@ -687,11 +784,13 @@ function designApply(project, opts = {}, runner = defaultRunner) {
687
784
  console.log(`[vitrine] dry-run: ${cmd.bin} -p <prompt ${cmd.prompt.length} chars> --permission-mode acceptEdits`);
688
785
  return 0;
689
786
  }
787
+ if (runner === defaultRunner) preflightClaude(cmd.bin);
690
788
  return runner(cmd);
691
789
  }
692
790
 
693
791
  // src/doctor.ts
694
- import { join as join8 } from "path";
792
+ import { readdirSync as readdirSync3 } from "fs";
793
+ import { join as join9 } from "path";
695
794
  function runDoctor(project, registry) {
696
795
  const paths = projectPaths(project.root);
697
796
  const issues = [];
@@ -702,12 +801,13 @@ function runDoctor(project, registry) {
702
801
  const configText = exists(paths.config) ? readText(paths.config) : "";
703
802
  const slotsText = exists(paths.slots) ? readText(paths.slots) : "";
704
803
  const paymentsText = exists(paths.payments) ? readText(paths.payments) : "";
804
+ const claudeText = exists(paths.claude) ? readText(paths.claude) : "";
705
805
  for (const core of ["@vitrine-kit/contracts", "@vitrine-kit/core"]) {
706
806
  if (!deps[core]) {
707
807
  add({ severity: "error", scope: "packages", message: `missing dependency ${core}`, fix: "add it to package.json" });
708
808
  }
709
809
  }
710
- if (exists(paths.claude) && !readText(paths.claude).includes("INSTRUCTION: apply the design")) {
810
+ if (exists(paths.claude) && !claudeText.includes("INSTRUCTION: apply the design")) {
711
811
  add({
712
812
  severity: "warn",
713
813
  scope: "design",
@@ -715,6 +815,50 @@ function runDoctor(project, registry) {
715
815
  fix: "update CLAUDE.md (kit update brings a fresh instruction)"
716
816
  });
717
817
  }
818
+ const markerPairs = [
819
+ { file: "site.config.ts", text: configText, start: "// vitrine:features:start", end: "// vitrine:features:end" },
820
+ { file: "site.config.ts", text: configText, start: "// vitrine:integrations:start", end: "// vitrine:integrations:end" },
821
+ ...exists(paths.claude) ? [{ file: "CLAUDE.md", text: claudeText, start: "<!-- vitrine:features:start -->", end: "<!-- vitrine:features:end -->" }] : []
822
+ ];
823
+ for (const m of markerPairs) {
824
+ const si = m.text.indexOf(m.start);
825
+ const ei = m.text.indexOf(m.end);
826
+ if (si === -1 || ei === -1 || ei < si) {
827
+ add({
828
+ severity: "error",
829
+ scope: "markers",
830
+ message: `${m.file}: managed markers "${m.start}" / "${m.end}" are missing or unpaired`,
831
+ fix: "restore the marker lines \u2014 add/update/remove cannot regenerate without them"
832
+ });
833
+ }
834
+ }
835
+ if (exists(join9(project.root, ".env"))) {
836
+ const gi = exists(join9(project.root, ".gitignore")) ? readText(join9(project.root, ".gitignore")) : "";
837
+ const covered = gi.split("\n").some((l) => /^\.env(\*)?$/.test(l.trim()));
838
+ if (!covered) {
839
+ add({
840
+ severity: "warn",
841
+ scope: "env",
842
+ message: '.env exists but .gitignore has no ".env" entry',
843
+ fix: 'add ".env" to .gitignore \u2014 secrets must not be committed'
844
+ });
845
+ }
846
+ }
847
+ if (isDir(paths.originals)) {
848
+ for (const entry of readdirSync3(paths.originals)) {
849
+ const at = entry.lastIndexOf("@");
850
+ const name = at > 0 ? entry.slice(0, at) : entry;
851
+ const version = at > 0 ? entry.slice(at + 1) : "";
852
+ if (project.lock.features[name]?.version !== version) {
853
+ add({
854
+ severity: "warn",
855
+ scope: "originals",
856
+ message: `orphaned snapshot "${entry}" (no installed feature@version matches)`,
857
+ fix: `delete .vitrine/originals/${entry}`
858
+ });
859
+ }
860
+ }
861
+ }
718
862
  for (const [name, pin] of Object.entries(project.lock.features)) {
719
863
  const scope = `feature:${name}`;
720
864
  if (!registry.hasFeature(name)) {
@@ -733,7 +877,7 @@ function runDoctor(project, registry) {
733
877
  const featDir = registry.featureDir(name);
734
878
  for (const map of manifest.files) {
735
879
  for (const file of eachFeatureFile(featDir, map)) {
736
- if (!exists(join8(project.root, file.repoRel))) {
880
+ if (!exists(join9(project.root, file.repoRel))) {
737
881
  add({
738
882
  severity: "error",
739
883
  scope,
@@ -783,7 +927,7 @@ function runDoctor(project, registry) {
783
927
 
784
928
  // src/update.ts
785
929
  import { rmSync as rmSync3 } from "fs";
786
- import { join as join9 } from "path";
930
+ import { join as join10 } from "path";
787
931
 
788
932
  // src/merge.ts
789
933
  function splitLines(s) {
@@ -927,15 +1071,15 @@ function planUpdate(project, name, registry) {
927
1071
  const fromVersion = pin.version;
928
1072
  const toVersion = manifest.kitVersion;
929
1073
  const featDir = registry.featureDir(name);
930
- const originalsBase = join9(projectPaths(project.root).originals, `${name}@${fromVersion}`);
1074
+ const originalsBase = join10(projectPaths(project.root).originals, `${name}@${fromVersion}`);
931
1075
  const files = [];
932
1076
  for (const map of manifest.files) {
933
1077
  for (const file of eachFeatureFile(featDir, map)) {
934
- const theirs = readText(file.srcAbs);
935
- const oursPath = join9(project.root, file.repoRel);
936
- const basePath = join9(originalsBase, file.repoRel);
937
- const ours = exists(oursPath) ? readText(oursPath) : null;
938
- const base = exists(basePath) ? readText(basePath) : null;
1078
+ const theirs = normalizeEol(readText(file.srcAbs));
1079
+ const oursPath = safeJoin(project.root, file.repoRel);
1080
+ const basePath = safeJoin(originalsBase, file.repoRel);
1081
+ const ours = exists(oursPath) ? normalizeEol(readText(oursPath)) : null;
1082
+ const base = exists(basePath) ? normalizeEol(readText(basePath)) : null;
939
1083
  if (ours === null) {
940
1084
  files.push({ to: file.toRel, status: "new", merged: theirs, conflicts: 0 });
941
1085
  } else if (theirs === base || theirs === ours) {
@@ -961,8 +1105,8 @@ function applyUpdate(project, plan, registry) {
961
1105
  const manifest = registry.loadManifest(plan.feature);
962
1106
  const featDir = registry.featureDir(plan.feature);
963
1107
  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}`);
1108
+ const newOriginals = join10(paths.originals, `${plan.feature}@${plan.toVersion}`);
1109
+ const oldOriginals = join10(paths.originals, `${plan.feature}@${plan.fromVersion}`);
966
1110
  const tx = new FsTransaction();
967
1111
  try {
968
1112
  for (const f of plan.files) {
@@ -978,6 +1122,7 @@ function applyUpdate(project, plan, registry) {
978
1122
  tx.commit();
979
1123
  } catch (error) {
980
1124
  tx.rollback();
1125
+ project.lock.features[plan.feature] = { version: plan.fromVersion };
981
1126
  throw error;
982
1127
  }
983
1128
  if (plan.fromVersion !== plan.toVersion) {
@@ -994,34 +1139,41 @@ function renderPlan(plan) {
994
1139
  }
995
1140
 
996
1141
  // src/commands.ts
997
- function requireProject() {
1142
+ function requireProject(explicitRoot) {
1143
+ if (explicitRoot) {
1144
+ const root2 = resolve5(explicitRoot);
1145
+ if (!exists(join11(root2, "vitrine.json"))) {
1146
+ throw new Error(`[vitrine] no vitrine.json in "${root2}" (--project must point at a client repo root)`);
1147
+ }
1148
+ return loadProject(root2);
1149
+ }
998
1150
  const root = findProjectRoot();
999
1151
  if (!root) {
1000
1152
  throw new Error("[vitrine] vitrine.json not found \u2014 not a Vitrine client repository");
1001
1153
  }
1002
1154
  return loadProject(root);
1003
1155
  }
1004
- function addFeatures(names, registryRoot) {
1005
- return installFeatures(requireProject(), names, createRegistrySource(registryRoot));
1156
+ function addFeatures(names, registryRoot, projectRoot) {
1157
+ return installFeatures(requireProject(projectRoot), names, createRegistrySource(registryRoot));
1006
1158
  }
1007
- function removeFeatureCmd(name, registryRoot) {
1008
- removeFeature(requireProject(), name, createRegistrySource(registryRoot));
1159
+ function removeFeatureCmd(name, registryRoot, projectRoot) {
1160
+ removeFeature(requireProject(projectRoot), name, createRegistrySource(registryRoot));
1009
1161
  }
1010
- function listFeatures(registryRoot) {
1011
- const project = requireProject();
1162
+ function listFeatures(registryRoot, projectRoot) {
1163
+ const project = requireProject(projectRoot);
1012
1164
  const registry = createRegistrySource(registryRoot);
1013
1165
  const installed = Object.keys(project.lock.features);
1014
1166
  const available = registry.listFeatures().filter((name) => !installed.includes(name));
1015
1167
  return { installed, available };
1016
1168
  }
1017
1169
  function designApplyCmd(opts = {}) {
1018
- return designApply(requireProject(), { bin: opts.bin, dryRun: opts.dryRun });
1170
+ return designApply(requireProject(opts.projectRoot), { bin: opts.bin, dryRun: opts.dryRun });
1019
1171
  }
1020
- function doctorCmd(registryRoot) {
1021
- return runDoctor(requireProject(), createRegistrySource(registryRoot));
1172
+ function doctorCmd(registryRoot, projectRoot) {
1173
+ return runDoctor(requireProject(projectRoot), createRegistrySource(registryRoot));
1022
1174
  }
1023
1175
  function updateFeaturesCmd(names, registryRoot, opts = {}) {
1024
- const project = requireProject();
1176
+ const project = requireProject(opts.projectRoot);
1025
1177
  const registry = createRegistrySource(registryRoot);
1026
1178
  const targets = names.length > 0 ? names : Object.keys(project.lock.features);
1027
1179
  return targets.map((name) => {
@@ -1031,38 +1183,40 @@ function updateFeaturesCmd(names, registryRoot, opts = {}) {
1031
1183
  return { plan, applied };
1032
1184
  });
1033
1185
  }
1034
- function diffFeatureCmd(name, registryRoot) {
1035
- return planUpdate(requireProject(), name, createRegistrySource(registryRoot));
1186
+ function diffFeatureCmd(name, registryRoot, projectRoot) {
1187
+ return planUpdate(requireProject(projectRoot), name, createRegistrySource(registryRoot));
1036
1188
  }
1037
1189
 
1038
1190
  // src/init.ts
1039
- import { readdirSync as readdirSync3 } from "fs";
1040
- import { join as join11 } from "path";
1191
+ import { readdirSync as readdirSync4 } from "fs";
1192
+ import { join as join13 } from "path";
1041
1193
 
1042
- // src/kit.ts
1043
- var KIT_VERSION = "0.0.0";
1194
+ // src/kit-versions.generated.ts
1195
+ var KIT_VERSION = "0.4.1";
1044
1196
  var CONTRACTS_VERSION = "1.0.0";
1045
1197
  var CONTRACTS_RANGE = "^1.0.0";
1046
- var CORE_RANGE = "^0.1.0";
1047
- var BLUEPRINT_RANGE = "^0.1.0";
1198
+ var CORE_RANGE = "^0.2.0";
1199
+ var BLUEPRINT_RANGE = "^0.2.0";
1200
+
1201
+ // src/kit.ts
1048
1202
  var REACT_RANGE = "^18.3.1";
1049
1203
  var CLIENT_REACT_RANGE = "^19.0.0";
1050
1204
  var NEXT_RANGE = "^15.1.0";
1051
1205
  var PAYLOAD_RANGE = "^3.0.0";
1052
1206
 
1053
1207
  // src/templates.ts
1054
- import { existsSync as existsSync7 } from "fs";
1055
- import { dirname as dirname5, join as join10 } from "path";
1208
+ import { existsSync as existsSync8 } from "fs";
1209
+ import { dirname as dirname6, join as join12 } from "path";
1056
1210
  function templatesRoot(registryRoot) {
1057
- return join10(dirname5(registryRoot), "templates");
1211
+ return join12(dirname6(registryRoot), "templates");
1058
1212
  }
1059
1213
  function hasTemplate(root, name) {
1060
- return existsSync7(join10(root, name, "files"));
1214
+ return existsSync8(join12(root, name, "files"));
1061
1215
  }
1062
1216
  function copyTemplate(root, name, destRoot) {
1063
- const filesDir = join10(root, name, "files");
1217
+ const filesDir = join12(root, name, "files");
1064
1218
  const rels = walkRelFiles(filesDir);
1065
- for (const rel of rels) writeText(join10(destRoot, rel), readText(join10(filesDir, rel)));
1219
+ for (const rel of rels) writeText(join12(destRoot, rel), readText(join12(filesDir, rel)));
1066
1220
  return rels;
1067
1221
  }
1068
1222
 
@@ -1077,7 +1231,7 @@ var PAYMENT_PROVIDER_FEATURES = [
1077
1231
  ];
1078
1232
  function suggestFeatures(tier, registry, backend = defaultBackend(tier)) {
1079
1233
  const core = ["catalog", "product-page", "seo"];
1080
- const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe", "reviews"];
1234
+ const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe"];
1081
1235
  const desired = tier === "catalog" ? core : [...core, ...shop];
1082
1236
  return desired.filter((name) => registry.hasFeature(name));
1083
1237
  }
@@ -1165,6 +1319,9 @@ function clientEnvExample(backend) {
1165
1319
  "# DB. Empty in dev \u2014 built-in SQLite (.vitrine/vendure.sqlite).",
1166
1320
  "DATABASE_URL=",
1167
1321
  "",
1322
+ "# Postgres password for docker compose (db service; compose default: vitrine).",
1323
+ "POSTGRES_PASSWORD=",
1324
+ "",
1168
1325
  "# Vendure Shop API (storefront \u2192 server).",
1169
1326
  "VENDURE_SHOP_API_URL=http://localhost:3001/shop-api",
1170
1327
  "",
@@ -1187,6 +1344,9 @@ function clientEnvExample(backend) {
1187
1344
  "# DB. For local dev you can leave it empty \u2014 a SQLite fallback is used (.vitrine/dev.sqlite).",
1188
1345
  "DATABASE_URL=",
1189
1346
  "",
1347
+ "# Postgres password for docker compose (db service; compose default: vitrine).",
1348
+ "POSTGRES_PASSWORD=",
1349
+ "",
1190
1350
  "# Payload secret (required; generate a random one for prod).",
1191
1351
  "PAYLOAD_SECRET=",
1192
1352
  "",
@@ -1254,8 +1414,8 @@ ${run}
1254
1414
 
1255
1415
  \`\`\`bash
1256
1416
  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)
1417
+ vitrine add cart # copy a feature: flag, slots, blueprint, env
1418
+ vitrine remove cart # remove (if the feature is removable)
1259
1419
  vitrine design apply # style the new feature
1260
1420
  \`\`\`
1261
1421
 
@@ -1294,10 +1454,10 @@ function scaffoldBase(opts) {
1294
1454
  const backendTemplate = `backend-${backend}`;
1295
1455
  if (hasTemplate(tRoot, backendTemplate)) copyTemplate(tRoot, backendTemplate, root);
1296
1456
  if (!baseCopied) {
1297
- writeText(join11(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1457
+ writeText(join13(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1298
1458
  }
1299
1459
  writeText(
1300
- join11(root, "vitrine.json"),
1460
+ join13(root, "vitrine.json"),
1301
1461
  `${JSON.stringify(
1302
1462
  { kitVersion: KIT_VERSION, contracts: CONTRACTS_VERSION, backend, tier, features: {} },
1303
1463
  null,
@@ -1306,7 +1466,7 @@ function scaffoldBase(opts) {
1306
1466
  `
1307
1467
  );
1308
1468
  writeText(
1309
- join11(root, "site.config.ts"),
1469
+ join13(root, "site.config.ts"),
1310
1470
  `import type { SiteConfig } from '@vitrine-kit/contracts';
1311
1471
 
1312
1472
  export const siteConfig: SiteConfig = {
@@ -1327,7 +1487,7 @@ export default siteConfig;
1327
1487
  `
1328
1488
  );
1329
1489
  writeText(
1330
- join11(root, "CLAUDE.md"),
1490
+ join13(root, "CLAUDE.md"),
1331
1491
  `# ${name}
1332
1492
 
1333
1493
  A Vitrine project. Backend: \`${backend}\`, tier: \`${tier}\`.
@@ -1393,16 +1553,16 @@ The step is idempotent: re-running converges and doesn't accumulate cruft.
1393
1553
  - **The user makes commits** \u2014 don't run \`git commit\`/\`git push\` without an explicit request.
1394
1554
  `
1395
1555
  );
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)}
1556
+ writeText(join13(root, "README.md"), clientReadme(name, backend, tier));
1557
+ writeText(join13(root, "lib", "slots.ts"), renderSlotsFile([]));
1558
+ writeText(join13(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1559
+ writeText(join13(root, "theme", "client.css"), renderNeutralTheme());
1560
+ writeText(join13(root, ".env.example"), clientEnvExample(backend));
1561
+ writeText(join13(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1402
1562
  `);
1403
1563
  }
1404
1564
  function initProject(opts) {
1405
- if (exists(opts.root) && readdirSync3(opts.root).length > 0) {
1565
+ if (exists(opts.root) && readdirSync4(opts.root).length > 0) {
1406
1566
  throw new Error(`[vitrine] directory "${opts.root}" is not empty`);
1407
1567
  }
1408
1568
  scaffoldBase(opts);
@@ -1412,22 +1572,21 @@ function initProject(opts) {
1412
1572
 
1413
1573
  // src/kit-update.ts
1414
1574
  import { spawnSync as spawnSync2 } from "child_process";
1415
- import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1575
+ import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync2 } from "fs";
1416
1576
  import { tmpdir } from "os";
1417
- import { join as join12, resolve as resolve5 } from "path";
1577
+ import { join as join14, resolve as resolve6 } from "path";
1418
1578
  var REPO = "vitrine-kit/vitrine";
1419
1579
  function hasBin(bin) {
1420
1580
  const probe = process.platform === "win32" ? "where" : "which";
1421
1581
  return spawnSync2(probe, [bin], { stdio: "ignore" }).status === 0;
1422
1582
  }
1423
- function acquireFromGh(version) {
1583
+ function acquireFromGh(tmp, version) {
1424
1584
  if (!hasBin("gh")) {
1425
1585
  throw new Error("[vitrine] network update needs gh (GitHub CLI), or pass --from <dir>");
1426
1586
  }
1427
1587
  if (!hasBin("tar")) {
1428
1588
  throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
1429
1589
  }
1430
- const tmp = mkdtempSync(join12(tmpdir(), "vitrine-kit-"));
1431
1590
  const dl = spawnSync2(
1432
1591
  "gh",
1433
1592
  ["release", "download", ...version ? [version] : [], "--repo", REPO, "--archive=tar.gz", "--dir", tmp, "--clobber"],
@@ -1436,22 +1595,41 @@ function acquireFromGh(version) {
1436
1595
  if (dl.status !== 0) {
1437
1596
  throw new Error("[vitrine] gh release download failed (check access/auth: gh auth status, and gh auth login if needed)");
1438
1597
  }
1439
- const tarball = readdirSync4(tmp).find((f) => f.endsWith(".tar.gz"));
1598
+ const tarball = readdirSync5(tmp).find((f) => f.endsWith(".tar.gz"));
1440
1599
  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) {
1600
+ if (spawnSync2("tar", ["-xzf", join14(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1442
1601
  throw new Error("[vitrine] tarball extraction failed");
1443
1602
  }
1444
- const root = readdirSync4(tmp).map((f) => join12(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync8(join12(p2, "registry", "_index.json")));
1603
+ const root = readdirSync5(tmp).map((f) => join14(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join14(p2, "registry", "_index.json")));
1445
1604
  if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
1446
1605
  return root;
1447
1606
  }
1607
+ function warnOnVersionMismatch(root, requested) {
1608
+ if (!requested) return;
1609
+ 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;
1612
+ const version = readJson(pkgFile).version;
1613
+ if (version && version !== tagSemver) {
1614
+ console.warn(`[vitrine] requested "${requested}" but the downloaded kit is @vitrine-kit/vitrine@${version}`);
1615
+ }
1616
+ }
1448
1617
  function kitUpdate(opts = {}) {
1449
- const source = opts.from ? resolve5(opts.from) : acquireFromGh(opts.version);
1450
- return populateCache(source, { home: opts.home, channel: opts.channel ?? "stable" });
1618
+ if (opts.from) {
1619
+ return populateCache(resolve6(opts.from), { home: opts.home, channel: opts.channel ?? "stable" });
1620
+ }
1621
+ const tmp = mkdtempSync(join14(tmpdir(), "vitrine-kit-"));
1622
+ try {
1623
+ const source = acquireFromGh(tmp, opts.version);
1624
+ warnOnVersionMismatch(source, opts.version);
1625
+ return populateCache(source, { home: opts.home, channel: opts.channel ?? "stable" });
1626
+ } finally {
1627
+ rmSync4(tmp, { recursive: true, force: true });
1628
+ }
1451
1629
  }
1452
1630
  function kitStatus(home = vitrineHome()) {
1453
1631
  const meta = readKitMeta(home);
1454
- const idxFile = join12(cachePaths(home).registry, "_index.json");
1632
+ const idxFile = join14(cachePaths(home).registry, "_index.json");
1455
1633
  const idx = exists(idxFile) ? readJson(idxFile) : null;
1456
1634
  return {
1457
1635
  cached: meta !== null,
@@ -1477,16 +1655,16 @@ function selfUpdate(opts = {}) {
1477
1655
  var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
1478
1656
  var program = new Command();
1479
1657
  program.name("vitrine").description("Vitrine CLI").version(pkg.version);
1480
- program.command("add").description("Add feature(s) to the current repository").argument("<features...>", "feature names").option("--registry <path>", "path to the registry").action((features, opts) => {
1481
- const res = addFeatures(features, opts.registry);
1658
+ program.command("add").description("Add feature(s) to the current repository").argument("<features...>", "feature names").option("--registry <path>", "path to the registry").option("--project <path>", "client repo root (default: walk up from cwd)").action((features, opts) => {
1659
+ const res = addFeatures(features, opts.registry, opts.project);
1482
1660
  console.log(res.installed.length ? `Installed: ${res.installed.join(", ")}` : "Already installed \u2014 no changes.");
1483
1661
  });
1484
- program.command("remove").description("Remove a feature (if removable)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").action((feature, opts) => {
1485
- removeFeatureCmd(feature, opts.registry);
1662
+ program.command("remove").description("Remove a feature (if removable)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").option("--project <path>", "client repo root (default: walk up from cwd)").action((feature, opts) => {
1663
+ removeFeatureCmd(feature, opts.registry, opts.project);
1486
1664
  console.log(`Removed: ${feature}`);
1487
1665
  });
1488
- program.command("update").description("Update an installed feature (3-way merge); all if no arguments").argument("[features...]", "feature names").option("--registry <path>", "path to the registry").option("--dry-run", "show the plan without writing").action((features, opts) => {
1489
- const outcomes = updateFeaturesCmd(features, opts.registry, { dryRun: opts.dryRun });
1666
+ program.command("update").description("Update an installed feature (3-way merge); all if no arguments").argument("[features...]", "feature names").option("--registry <path>", "path to the registry").option("--dry-run", "show the plan without writing").option("--project <path>", "client repo root (default: walk up from cwd)").action((features, opts) => {
1667
+ const outcomes = updateFeaturesCmd(features, opts.registry, { dryRun: opts.dryRun, projectRoot: opts.project });
1490
1668
  let conflicts = 0;
1491
1669
  for (const { plan, applied } of outcomes) {
1492
1670
  console.log(renderPlan(plan));
@@ -1498,17 +1676,17 @@ program.command("update").description("Update an installed feature (3-way merge)
1498
1676
  \u26A0 conflicts: ${conflicts}. Resolve the git markers (<<<<<<< / >>>>>>>) by hand.`);
1499
1677
  }
1500
1678
  });
1501
- program.command("diff").description("Preview update changes (without writing)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").action((feature, opts) => {
1502
- console.log(renderPlan(diffFeatureCmd(feature, opts.registry)));
1679
+ program.command("diff").description("Preview update changes (without writing)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").option("--project <path>", "client repo root (default: walk up from cwd)").action((feature, opts) => {
1680
+ console.log(renderPlan(diffFeatureCmd(feature, opts.registry, opts.project)));
1503
1681
  });
1504
- program.command("list").description("Installed and available features").option("--registry <path>", "path to the registry").action((opts) => {
1505
- const { installed, available } = listFeatures(opts.registry);
1682
+ program.command("list").description("Installed and available features").option("--registry <path>", "path to the registry").option("--project <path>", "client repo root (default: walk up from cwd)").action((opts) => {
1683
+ const { installed, available } = listFeatures(opts.registry, opts.project);
1506
1684
  console.log("Installed:", installed.join(", ") || "\u2014");
1507
1685
  console.log("Available:", available.join(", ") || "\u2014");
1508
1686
  });
1509
1687
  var design = program.command("design").description("AI design step (wrapper over Claude Code)");
1510
- design.command("apply").description("Apply the design from /design to the tokens (via Claude Code)").option("--bin <path>", "path to Claude Code (otherwise VITRINE_CLAUDE_BIN / PATH)").option("--dry-run", "show the command without running").action((opts) => {
1511
- const code = designApplyCmd({ bin: opts.bin, dryRun: opts.dryRun });
1688
+ design.command("apply").description("Apply the design from /design to the tokens (via Claude Code)").option("--bin <path>", "path to Claude Code (otherwise VITRINE_CLAUDE_BIN / PATH)").option("--dry-run", "show the command without running").option("--project <path>", "client repo root (default: walk up from cwd)").action((opts) => {
1689
+ const code = designApplyCmd({ bin: opts.bin, dryRun: opts.dryRun, projectRoot: opts.project });
1512
1690
  if (code !== 0) process.exit(code);
1513
1691
  });
1514
1692
  var kit = program.command("kit").description("Local tooling (the ~/.vitrine registry cache)");
@@ -1530,8 +1708,8 @@ program.command("self-update").description("Update the CLI itself (@vitrine-kit/
1530
1708
  const code = selfUpdate({ dryRun: opts.dryRun });
1531
1709
  if (code !== 0) process.exit(code);
1532
1710
  });
1533
- program.command("doctor").description("Check consistency: vitrine.json \u2194 files \u2194 packages \u2194 env").option("--registry <path>", "path to the registry").action((opts) => {
1534
- const report = doctorCmd(opts.registry);
1711
+ program.command("doctor").description("Check consistency: vitrine.json \u2194 files \u2194 packages \u2194 env").option("--registry <path>", "path to the registry").option("--project <path>", "client repo root (default: walk up from cwd)").action((opts) => {
1712
+ const report = doctorCmd(opts.registry, opts.project);
1535
1713
  if (report.issues.length === 0) {
1536
1714
  console.log("\u2713 No issues found.");
1537
1715
  return;
@@ -1595,7 +1773,7 @@ program.command("init").description("Create a new client repository").argument("
1595
1773
  const finalTier = tier ?? "catalog";
1596
1774
  const backend = opts.backend ?? defaultBackend(finalTier);
1597
1775
  const finalFeatures = features ?? suggestFeatures(finalTier, registry, backend);
1598
- const root = resolve6(String(opts.dir ?? process.cwd()), name);
1776
+ const root = resolve7(String(opts.dir ?? process.cwd()), name);
1599
1777
  const res = initProject({ root, name, backend, tier: finalTier, features: finalFeatures, registry });
1600
1778
  console.log(`Created project "${name}" \u2192 ${root}`);
1601
1779
  console.log(`Installed: ${res.installed.join(", ") || "\u2014"}`);