@vitrine-kit/vitrine 0.4.0 → 0.4.3

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 (39) hide show
  1. package/dist/index.js +247 -87
  2. package/kit/registry/_index.json +9 -9
  3. package/kit/registry/cart/feature.json +1 -1
  4. package/kit/registry/cart/files/app/api/cart/route.ts +58 -6
  5. package/kit/registry/cart/files/components/cart/AddToCart.tsx +106 -14
  6. package/kit/registry/cart/files/components/cart/CartView.tsx +19 -1
  7. package/kit/registry/catalog/feature.json +1 -1
  8. package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +5 -1
  9. package/kit/registry/checkout/feature.json +1 -1
  10. package/kit/registry/checkout/files/app/api/checkout/route.ts +11 -1
  11. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +25 -10
  12. package/kit/registry/checkout-paddle/docs/checkout-paddle.md +5 -2
  13. package/kit/registry/checkout-paddle/feature.json +1 -1
  14. package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +9 -2
  15. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +25 -4
  16. package/kit/registry/checkout-stripe/docs/checkout-stripe.md +3 -1
  17. package/kit/registry/checkout-stripe/feature.json +1 -1
  18. package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +9 -2
  19. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +10 -3
  20. package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +5 -2
  21. package/kit/registry/checkout-yookassa/feature.json +1 -1
  22. package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +9 -2
  23. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +14 -4
  24. package/kit/registry/product-page/feature.json +1 -1
  25. package/kit/registry/product-page/files/components/product/ProductView.tsx +5 -1
  26. package/kit/registry/seo/feature.json +1 -1
  27. package/kit/templates/backend-payload/files/lib/seed/admin.ts +9 -3
  28. package/kit/templates/backend-payload/files/lib/seed/demo.ts +74 -28
  29. package/kit/templates/backend-payload/files/lib/seed/run.ts +35 -10
  30. package/kit/templates/backend-payload/files/next.config.mjs +28 -0
  31. package/kit/templates/backend-payload/files/payload.config.ts +7 -1
  32. package/kit/templates/backend-payload/files/seed-assets/placeholder-1b.svg +6 -0
  33. package/kit/templates/backend-payload/files/seed-assets/placeholder-2b.svg +6 -0
  34. package/kit/templates/backend-vendure/files/vendure-config.ts +38 -1
  35. package/kit/templates/base/files/.gitattributes +12 -0
  36. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +6 -1
  37. package/kit/templates/base/files/app/(frontend)/page.tsx +36 -3
  38. package/kit/templates/base/files/next.config.mjs +28 -0
  39. package/package.json +5 -2
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)
@@ -137,8 +153,9 @@ function bundledKitRoot() {
137
153
  }
138
154
 
139
155
  // src/cache.ts
140
- import { cpSync, existsSync as existsSync4, rmSync } from "fs";
156
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2, renameSync, rmSync } from "fs";
141
157
  import { join as join4, resolve as resolve3 } from "path";
158
+ import { registryIndexSchema } from "@vitrine-kit/contracts";
142
159
  function vitrineHome() {
143
160
  if (process.env.VITRINE_HOME) return resolve3(process.env.VITRINE_HOME);
144
161
  const home = process.env.USERPROFILE ?? process.env.HOME;
@@ -198,11 +215,22 @@ function populateCache(fromDir, opts = {}) {
198
215
  }
199
216
  const paths = cachePaths(home);
200
217
  const oldIndex = readIndex(paths.registry);
201
- rmSync(paths.registry, { recursive: true, force: true });
202
- cpSync(srcRegistry, paths.registry, { recursive: true });
203
- if (existsSync4(srcTemplates)) {
204
- rmSync(paths.templates, { recursive: true, force: true });
205
- 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 });
206
234
  }
207
235
  const newIndex = readIndex(paths.registry);
208
236
  const kitVersion = newIndex?.kitVersion ?? "0.0.0";
@@ -247,7 +275,10 @@ function resolveRegistryRoot(explicit) {
247
275
  function createRegistrySource(explicitRoot) {
248
276
  const root = resolveRegistryRoot(explicitRoot);
249
277
  const cache = /* @__PURE__ */ new Map();
250
- const featureDir = (name) => join5(root, name);
278
+ const featureDir = (name) => {
279
+ assertFeatureName(name);
280
+ return join5(root, name);
281
+ };
251
282
  const hasFeature = (name) => existsSync5(join5(featureDir(name), "feature.json"));
252
283
  const loadManifest = (name) => {
253
284
  const cached = cache.get(name);
@@ -255,6 +286,10 @@ function createRegistrySource(explicitRoot) {
255
286
  const file = join5(featureDir(name), "feature.json");
256
287
  if (!existsSync5(file)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
257
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
+ }
258
293
  cache.set(name, manifest);
259
294
  return manifest;
260
295
  };
@@ -269,7 +304,7 @@ function createRegistrySource(explicitRoot) {
269
304
  import { join as join7 } from "path";
270
305
 
271
306
  // src/transaction.ts
272
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
307
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
273
308
  import { dirname as dirname5 } from "path";
274
309
  var FsTransaction = class {
275
310
  backups = [];
@@ -280,8 +315,8 @@ var FsTransaction = class {
280
315
  }
281
316
  write(path, content) {
282
317
  this.snapshot(path);
283
- mkdirSync2(dirname5(path), { recursive: true });
284
- writeFileSync2(path, content, "utf8");
318
+ mkdirSync3(dirname5(path), { recursive: true });
319
+ writeFileSync2(path, normalizeEol(content), "utf8");
285
320
  }
286
321
  remove(path) {
287
322
  this.snapshot(path);
@@ -467,7 +502,7 @@ function renderNeutralTheme() {
467
502
  // src/feature-files.ts
468
503
  import { join as join6 } from "path";
469
504
  function* eachFeatureFile(featDir, map) {
470
- const src = join6(featDir, map.from);
505
+ const src = safeJoin(featDir, map.from);
471
506
  if (!exists(src)) return;
472
507
  const rels = isDir(src) ? walkRelFiles(src) : [""];
473
508
  for (const rel of rels) {
@@ -481,27 +516,33 @@ function* eachFeatureFile(featDir, map) {
481
516
  function resolveOrder(names, registry) {
482
517
  const order = [];
483
518
  const seen = /* @__PURE__ */ new Set();
519
+ const visiting = /* @__PURE__ */ new Set();
484
520
  const visit = (name) => {
485
521
  if (seen.has(name)) return;
522
+ if (visiting.has(name)) {
523
+ throw new Error(`[vitrine] circular registryDependencies involving "${name}"`);
524
+ }
486
525
  if (!registry.hasFeature(name)) {
487
526
  throw new Error(`[vitrine] feature "${name}" not found in the registry`);
488
527
  }
489
- seen.add(name);
528
+ visiting.add(name);
490
529
  const manifest = registry.loadManifest(name);
491
530
  for (const dep of manifest.registryDependencies ?? []) visit(dep);
531
+ visiting.delete(name);
532
+ seen.add(name);
492
533
  order.push(name);
493
534
  };
494
535
  for (const name of names) visit(name);
495
536
  return order;
496
537
  }
497
- function validate(name, manifest, project) {
538
+ function validate(name, manifest, project, present) {
498
539
  if (!manifest.tier.includes(project.lock.tier)) {
499
540
  throw new Error(
500
541
  `[vitrine] feature "${name}" does not support tier "${project.lock.tier}" (only ${manifest.tier.join(", ")})`
501
542
  );
502
543
  }
503
544
  for (const conflict of manifest.conflicts ?? []) {
504
- if (project.lock.features[conflict]) {
545
+ if (present.has(conflict)) {
505
546
  throw new Error(`[vitrine] conflict: "${name}" is incompatible with the installed "${conflict}"`);
506
547
  }
507
548
  }
@@ -510,9 +551,6 @@ function copyFeatureFiles(project, name, manifest, registry, tx) {
510
551
  const featDir = registry.featureDir(name);
511
552
  const originalsBase = join7(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
512
553
  for (const map of manifest.files) {
513
- if (!exists(join7(featDir, map.from))) {
514
- throw new Error(`[vitrine] feature "${name}": no source "${map.from}"`);
515
- }
516
554
  for (const file of eachFeatureFile(featDir, map)) {
517
555
  const content = readText(file.srcAbs);
518
556
  tx.write(safeJoin(project.root, file.repoRel), content);
@@ -574,11 +612,21 @@ function installFeatures(project, names, registry) {
574
612
  if (toInstall.length === 0) {
575
613
  return { installed: [], skipped: order };
576
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
+ }
577
626
  const tx = new FsTransaction();
578
627
  try {
579
628
  for (const name of toInstall) {
580
629
  const manifest = registry.loadManifest(name);
581
- validate(name, manifest, project);
582
630
  copyFeatureFiles(project, name, manifest, registry, tx);
583
631
  project.lock.features[name] = { version: manifest.kitVersion };
584
632
  }
@@ -623,6 +671,20 @@ function removeFeature(project, name, registry) {
623
671
  }
624
672
  delete project.lock.features[name];
625
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
+ }
626
688
  tx.commit();
627
689
  } catch (error) {
628
690
  tx.rollback();
@@ -637,6 +699,7 @@ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
637
699
  import { delimiter, join as join8 } from "path";
638
700
  import { TOKEN_CSS_VARS as TOKEN_CSS_VARS2 } from "@vitrine-kit/contracts";
639
701
  var DESIGN_HEADING = "## INSTRUCTION: apply the design from /design";
702
+ var MAX_INSTRUCTION_CHARS = 4e3;
640
703
  function findClaudeBin(explicit) {
641
704
  const pinned = explicit ?? process.env.VITRINE_CLAUDE_BIN;
642
705
  if (pinned) {
@@ -667,7 +730,11 @@ function extractDesignInstruction(claudeMd) {
667
730
  }
668
731
  function buildDesignPrompt(project) {
669
732
  const claudeMd = existsSync7(join8(project.root, "CLAUDE.md")) ? readText(join8(project.root, "CLAUDE.md")) : "";
670
- const instruction = extractDesignInstruction(claudeMd) ?? DESIGN_HEADING;
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
+ }
671
738
  const tokens = TOKEN_CSS_VARS2.map((v) => ` ${v}`).join("\n");
672
739
  return [
673
740
  instruction,
@@ -687,6 +754,19 @@ var defaultRunner = ({ bin, args, cwd }) => {
687
754
  if (res.error) throw res.error;
688
755
  return res.status ?? 0;
689
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
+ }
690
770
  function planDesignApply(project, opts = {}) {
691
771
  const bin = findClaudeBin(opts.bin);
692
772
  const prompt = buildDesignPrompt(project);
@@ -704,10 +784,12 @@ function designApply(project, opts = {}, runner = defaultRunner) {
704
784
  console.log(`[vitrine] dry-run: ${cmd.bin} -p <prompt ${cmd.prompt.length} chars> --permission-mode acceptEdits`);
705
785
  return 0;
706
786
  }
787
+ if (runner === defaultRunner) preflightClaude(cmd.bin);
707
788
  return runner(cmd);
708
789
  }
709
790
 
710
791
  // src/doctor.ts
792
+ import { readdirSync as readdirSync3 } from "fs";
711
793
  import { join as join9 } from "path";
712
794
  function runDoctor(project, registry) {
713
795
  const paths = projectPaths(project.root);
@@ -719,12 +801,13 @@ function runDoctor(project, registry) {
719
801
  const configText = exists(paths.config) ? readText(paths.config) : "";
720
802
  const slotsText = exists(paths.slots) ? readText(paths.slots) : "";
721
803
  const paymentsText = exists(paths.payments) ? readText(paths.payments) : "";
804
+ const claudeText = exists(paths.claude) ? readText(paths.claude) : "";
722
805
  for (const core of ["@vitrine-kit/contracts", "@vitrine-kit/core"]) {
723
806
  if (!deps[core]) {
724
807
  add({ severity: "error", scope: "packages", message: `missing dependency ${core}`, fix: "add it to package.json" });
725
808
  }
726
809
  }
727
- if (exists(paths.claude) && !readText(paths.claude).includes("INSTRUCTION: apply the design")) {
810
+ if (exists(paths.claude) && !claudeText.includes("INSTRUCTION: apply the design")) {
728
811
  add({
729
812
  severity: "warn",
730
813
  scope: "design",
@@ -732,6 +815,50 @@ function runDoctor(project, registry) {
732
815
  fix: "update CLAUDE.md (kit update brings a fresh instruction)"
733
816
  });
734
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
+ }
735
862
  for (const [name, pin] of Object.entries(project.lock.features)) {
736
863
  const scope = `feature:${name}`;
737
864
  if (!registry.hasFeature(name)) {
@@ -948,11 +1075,11 @@ function planUpdate(project, name, registry) {
948
1075
  const files = [];
949
1076
  for (const map of manifest.files) {
950
1077
  for (const file of eachFeatureFile(featDir, map)) {
951
- const theirs = readText(file.srcAbs);
952
- const oursPath = join10(project.root, file.repoRel);
953
- const basePath = join10(originalsBase, file.repoRel);
954
- const ours = exists(oursPath) ? readText(oursPath) : null;
955
- 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;
956
1083
  if (ours === null) {
957
1084
  files.push({ to: file.toRel, status: "new", merged: theirs, conflicts: 0 });
958
1085
  } else if (theirs === base || theirs === ours) {
@@ -995,6 +1122,7 @@ function applyUpdate(project, plan, registry) {
995
1122
  tx.commit();
996
1123
  } catch (error) {
997
1124
  tx.rollback();
1125
+ project.lock.features[plan.feature] = { version: plan.fromVersion };
998
1126
  throw error;
999
1127
  }
1000
1128
  if (plan.fromVersion !== plan.toVersion) {
@@ -1011,34 +1139,41 @@ function renderPlan(plan) {
1011
1139
  }
1012
1140
 
1013
1141
  // src/commands.ts
1014
- 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
+ }
1015
1150
  const root = findProjectRoot();
1016
1151
  if (!root) {
1017
1152
  throw new Error("[vitrine] vitrine.json not found \u2014 not a Vitrine client repository");
1018
1153
  }
1019
1154
  return loadProject(root);
1020
1155
  }
1021
- function addFeatures(names, registryRoot) {
1022
- return installFeatures(requireProject(), names, createRegistrySource(registryRoot));
1156
+ function addFeatures(names, registryRoot, projectRoot) {
1157
+ return installFeatures(requireProject(projectRoot), names, createRegistrySource(registryRoot));
1023
1158
  }
1024
- function removeFeatureCmd(name, registryRoot) {
1025
- removeFeature(requireProject(), name, createRegistrySource(registryRoot));
1159
+ function removeFeatureCmd(name, registryRoot, projectRoot) {
1160
+ removeFeature(requireProject(projectRoot), name, createRegistrySource(registryRoot));
1026
1161
  }
1027
- function listFeatures(registryRoot) {
1028
- const project = requireProject();
1162
+ function listFeatures(registryRoot, projectRoot) {
1163
+ const project = requireProject(projectRoot);
1029
1164
  const registry = createRegistrySource(registryRoot);
1030
1165
  const installed = Object.keys(project.lock.features);
1031
1166
  const available = registry.listFeatures().filter((name) => !installed.includes(name));
1032
1167
  return { installed, available };
1033
1168
  }
1034
1169
  function designApplyCmd(opts = {}) {
1035
- return designApply(requireProject(), { bin: opts.bin, dryRun: opts.dryRun });
1170
+ return designApply(requireProject(opts.projectRoot), { bin: opts.bin, dryRun: opts.dryRun });
1036
1171
  }
1037
- function doctorCmd(registryRoot) {
1038
- return runDoctor(requireProject(), createRegistrySource(registryRoot));
1172
+ function doctorCmd(registryRoot, projectRoot) {
1173
+ return runDoctor(requireProject(projectRoot), createRegistrySource(registryRoot));
1039
1174
  }
1040
1175
  function updateFeaturesCmd(names, registryRoot, opts = {}) {
1041
- const project = requireProject();
1176
+ const project = requireProject(opts.projectRoot);
1042
1177
  const registry = createRegistrySource(registryRoot);
1043
1178
  const targets = names.length > 0 ? names : Object.keys(project.lock.features);
1044
1179
  return targets.map((name) => {
@@ -1048,19 +1183,19 @@ function updateFeaturesCmd(names, registryRoot, opts = {}) {
1048
1183
  return { plan, applied };
1049
1184
  });
1050
1185
  }
1051
- function diffFeatureCmd(name, registryRoot) {
1052
- return planUpdate(requireProject(), name, createRegistrySource(registryRoot));
1186
+ function diffFeatureCmd(name, registryRoot, projectRoot) {
1187
+ return planUpdate(requireProject(projectRoot), name, createRegistrySource(registryRoot));
1053
1188
  }
1054
1189
 
1055
1190
  // src/init.ts
1056
- import { readdirSync as readdirSync3 } from "fs";
1057
- import { join as join12 } from "path";
1191
+ import { readdirSync as readdirSync4 } from "fs";
1192
+ import { join as join13 } from "path";
1058
1193
 
1059
1194
  // src/kit-versions.generated.ts
1060
- var KIT_VERSION = "0.4.0";
1195
+ var KIT_VERSION = "0.4.3";
1061
1196
  var CONTRACTS_VERSION = "1.0.0";
1062
1197
  var CONTRACTS_RANGE = "^1.0.0";
1063
- var CORE_RANGE = "^0.2.0";
1198
+ var CORE_RANGE = "^0.3.0";
1064
1199
  var BLUEPRINT_RANGE = "^0.2.0";
1065
1200
 
1066
1201
  // src/kit.ts
@@ -1071,17 +1206,17 @@ var PAYLOAD_RANGE = "^3.0.0";
1071
1206
 
1072
1207
  // src/templates.ts
1073
1208
  import { existsSync as existsSync8 } from "fs";
1074
- import { dirname as dirname6, join as join11 } from "path";
1209
+ import { dirname as dirname6, join as join12 } from "path";
1075
1210
  function templatesRoot(registryRoot) {
1076
- return join11(dirname6(registryRoot), "templates");
1211
+ return join12(dirname6(registryRoot), "templates");
1077
1212
  }
1078
1213
  function hasTemplate(root, name) {
1079
- return existsSync8(join11(root, name, "files"));
1214
+ return existsSync8(join12(root, name, "files"));
1080
1215
  }
1081
1216
  function copyTemplate(root, name, destRoot) {
1082
- const filesDir = join11(root, name, "files");
1217
+ const filesDir = join12(root, name, "files");
1083
1218
  const rels = walkRelFiles(filesDir);
1084
- for (const rel of rels) writeText(join11(destRoot, rel), readText(join11(filesDir, rel)));
1219
+ for (const rel of rels) writeText(join12(destRoot, rel), readText(join12(filesDir, rel)));
1085
1220
  return rels;
1086
1221
  }
1087
1222
 
@@ -1184,6 +1319,9 @@ function clientEnvExample(backend) {
1184
1319
  "# DB. Empty in dev \u2014 built-in SQLite (.vitrine/vendure.sqlite).",
1185
1320
  "DATABASE_URL=",
1186
1321
  "",
1322
+ "# Postgres password for docker compose (db service; compose default: vitrine).",
1323
+ "POSTGRES_PASSWORD=",
1324
+ "",
1187
1325
  "# Vendure Shop API (storefront \u2192 server).",
1188
1326
  "VENDURE_SHOP_API_URL=http://localhost:3001/shop-api",
1189
1327
  "",
@@ -1206,6 +1344,9 @@ function clientEnvExample(backend) {
1206
1344
  "# DB. For local dev you can leave it empty \u2014 a SQLite fallback is used (.vitrine/dev.sqlite).",
1207
1345
  "DATABASE_URL=",
1208
1346
  "",
1347
+ "# Postgres password for docker compose (db service; compose default: vitrine).",
1348
+ "POSTGRES_PASSWORD=",
1349
+ "",
1209
1350
  "# Payload secret (required; generate a random one for prod).",
1210
1351
  "PAYLOAD_SECRET=",
1211
1352
  "",
@@ -1244,8 +1385,9 @@ function clientReadme(name, backend, tier) {
1244
1385
  "- Admin: http://localhost:3000/admin",
1245
1386
  "",
1246
1387
  "Without Postgres, dev starts a built-in SQLite (`.vitrine/dev.sqlite`), seeds a",
1247
- "demo catalog (5 products, 2 categories) and creates a dev admin (login/password printed",
1248
- "to the console once). Disable the fallback in dev too with `VITRINE_DB_STRICT=1`."
1388
+ "demo catalog (5 products, 2 categories, size/color variants, gallery images) and creates a",
1389
+ "dev admin (login/password printed to the console once). Disable the fallback in",
1390
+ "dev too with `VITRINE_DB_STRICT=1`."
1249
1391
  ].join("\n");
1250
1392
  const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... # Vendure cookie secret" : "export PAYLOAD_SECRET=... # random Payload secret";
1251
1393
  return `# ${name}
@@ -1313,10 +1455,10 @@ function scaffoldBase(opts) {
1313
1455
  const backendTemplate = `backend-${backend}`;
1314
1456
  if (hasTemplate(tRoot, backendTemplate)) copyTemplate(tRoot, backendTemplate, root);
1315
1457
  if (!baseCopied) {
1316
- writeText(join12(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1458
+ writeText(join13(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1317
1459
  }
1318
1460
  writeText(
1319
- join12(root, "vitrine.json"),
1461
+ join13(root, "vitrine.json"),
1320
1462
  `${JSON.stringify(
1321
1463
  { kitVersion: KIT_VERSION, contracts: CONTRACTS_VERSION, backend, tier, features: {} },
1322
1464
  null,
@@ -1325,7 +1467,7 @@ function scaffoldBase(opts) {
1325
1467
  `
1326
1468
  );
1327
1469
  writeText(
1328
- join12(root, "site.config.ts"),
1470
+ join13(root, "site.config.ts"),
1329
1471
  `import type { SiteConfig } from '@vitrine-kit/contracts';
1330
1472
 
1331
1473
  export const siteConfig: SiteConfig = {
@@ -1346,7 +1488,7 @@ export default siteConfig;
1346
1488
  `
1347
1489
  );
1348
1490
  writeText(
1349
- join12(root, "CLAUDE.md"),
1491
+ join13(root, "CLAUDE.md"),
1350
1492
  `# ${name}
1351
1493
 
1352
1494
  A Vitrine project. Backend: \`${backend}\`, tier: \`${tier}\`.
@@ -1412,16 +1554,16 @@ The step is idempotent: re-running converges and doesn't accumulate cruft.
1412
1554
  - **The user makes commits** \u2014 don't run \`git commit\`/\`git push\` without an explicit request.
1413
1555
  `
1414
1556
  );
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)}
1557
+ writeText(join13(root, "README.md"), clientReadme(name, backend, tier));
1558
+ writeText(join13(root, "lib", "slots.ts"), renderSlotsFile([]));
1559
+ writeText(join13(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1560
+ writeText(join13(root, "theme", "client.css"), renderNeutralTheme());
1561
+ writeText(join13(root, ".env.example"), clientEnvExample(backend));
1562
+ writeText(join13(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1421
1563
  `);
1422
1564
  }
1423
1565
  function initProject(opts) {
1424
- if (exists(opts.root) && readdirSync3(opts.root).length > 0) {
1566
+ if (exists(opts.root) && readdirSync4(opts.root).length > 0) {
1425
1567
  throw new Error(`[vitrine] directory "${opts.root}" is not empty`);
1426
1568
  }
1427
1569
  scaffoldBase(opts);
@@ -1431,22 +1573,21 @@ function initProject(opts) {
1431
1573
 
1432
1574
  // src/kit-update.ts
1433
1575
  import { spawnSync as spawnSync2 } from "child_process";
1434
- import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1576
+ import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync2 } from "fs";
1435
1577
  import { tmpdir } from "os";
1436
- import { join as join13, resolve as resolve5 } from "path";
1578
+ import { join as join14, resolve as resolve6 } from "path";
1437
1579
  var REPO = "vitrine-kit/vitrine";
1438
1580
  function hasBin(bin) {
1439
1581
  const probe = process.platform === "win32" ? "where" : "which";
1440
1582
  return spawnSync2(probe, [bin], { stdio: "ignore" }).status === 0;
1441
1583
  }
1442
- function acquireFromGh(version) {
1584
+ function acquireFromGh(tmp, version) {
1443
1585
  if (!hasBin("gh")) {
1444
1586
  throw new Error("[vitrine] network update needs gh (GitHub CLI), or pass --from <dir>");
1445
1587
  }
1446
1588
  if (!hasBin("tar")) {
1447
1589
  throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
1448
1590
  }
1449
- const tmp = mkdtempSync(join13(tmpdir(), "vitrine-kit-"));
1450
1591
  const dl = spawnSync2(
1451
1592
  "gh",
1452
1593
  ["release", "download", ...version ? [version] : [], "--repo", REPO, "--archive=tar.gz", "--dir", tmp, "--clobber"],
@@ -1455,22 +1596,41 @@ function acquireFromGh(version) {
1455
1596
  if (dl.status !== 0) {
1456
1597
  throw new Error("[vitrine] gh release download failed (check access/auth: gh auth status, and gh auth login if needed)");
1457
1598
  }
1458
- const tarball = readdirSync4(tmp).find((f) => f.endsWith(".tar.gz"));
1599
+ const tarball = readdirSync5(tmp).find((f) => f.endsWith(".tar.gz"));
1459
1600
  if (!tarball) throw new Error("[vitrine] release tarball not found after download");
1460
- if (spawnSync2("tar", ["-xzf", join13(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1601
+ if (spawnSync2("tar", ["-xzf", join14(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1461
1602
  throw new Error("[vitrine] tarball extraction failed");
1462
1603
  }
1463
- const root = readdirSync4(tmp).map((f) => join13(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join13(p2, "registry", "_index.json")));
1604
+ const root = readdirSync5(tmp).map((f) => join14(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join14(p2, "registry", "_index.json")));
1464
1605
  if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
1465
1606
  return root;
1466
1607
  }
1608
+ function warnOnVersionMismatch(root, requested) {
1609
+ if (!requested) return;
1610
+ const tagSemver = requested.match(/^@vitrine-kit\/vitrine@(.+)$/)?.[1] ?? requested.match(/^v?(\d+\.\d+\.\d+\S*)$/)?.[1];
1611
+ const pkgFile = join14(root, "packages", "cli", "package.json");
1612
+ if (!tagSemver || !exists(pkgFile)) return;
1613
+ const version = readJson(pkgFile).version;
1614
+ if (version && version !== tagSemver) {
1615
+ console.warn(`[vitrine] requested "${requested}" but the downloaded kit is @vitrine-kit/vitrine@${version}`);
1616
+ }
1617
+ }
1467
1618
  function kitUpdate(opts = {}) {
1468
- const source = opts.from ? resolve5(opts.from) : acquireFromGh(opts.version);
1469
- return populateCache(source, { home: opts.home, channel: opts.channel ?? "stable" });
1619
+ if (opts.from) {
1620
+ return populateCache(resolve6(opts.from), { home: opts.home, channel: opts.channel ?? "stable" });
1621
+ }
1622
+ const tmp = mkdtempSync(join14(tmpdir(), "vitrine-kit-"));
1623
+ try {
1624
+ const source = acquireFromGh(tmp, opts.version);
1625
+ warnOnVersionMismatch(source, opts.version);
1626
+ return populateCache(source, { home: opts.home, channel: opts.channel ?? "stable" });
1627
+ } finally {
1628
+ rmSync4(tmp, { recursive: true, force: true });
1629
+ }
1470
1630
  }
1471
1631
  function kitStatus(home = vitrineHome()) {
1472
1632
  const meta = readKitMeta(home);
1473
- const idxFile = join13(cachePaths(home).registry, "_index.json");
1633
+ const idxFile = join14(cachePaths(home).registry, "_index.json");
1474
1634
  const idx = exists(idxFile) ? readJson(idxFile) : null;
1475
1635
  return {
1476
1636
  cached: meta !== null,
@@ -1496,16 +1656,16 @@ function selfUpdate(opts = {}) {
1496
1656
  var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
1497
1657
  var program = new Command();
1498
1658
  program.name("vitrine").description("Vitrine CLI").version(pkg.version);
1499
- 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) => {
1500
- const res = addFeatures(features, opts.registry);
1659
+ 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) => {
1660
+ const res = addFeatures(features, opts.registry, opts.project);
1501
1661
  console.log(res.installed.length ? `Installed: ${res.installed.join(", ")}` : "Already installed \u2014 no changes.");
1502
1662
  });
1503
- program.command("remove").description("Remove a feature (if removable)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").action((feature, opts) => {
1504
- removeFeatureCmd(feature, opts.registry);
1663
+ 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) => {
1664
+ removeFeatureCmd(feature, opts.registry, opts.project);
1505
1665
  console.log(`Removed: ${feature}`);
1506
1666
  });
1507
- 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) => {
1508
- const outcomes = updateFeaturesCmd(features, opts.registry, { dryRun: opts.dryRun });
1667
+ 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) => {
1668
+ const outcomes = updateFeaturesCmd(features, opts.registry, { dryRun: opts.dryRun, projectRoot: opts.project });
1509
1669
  let conflicts = 0;
1510
1670
  for (const { plan, applied } of outcomes) {
1511
1671
  console.log(renderPlan(plan));
@@ -1517,17 +1677,17 @@ program.command("update").description("Update an installed feature (3-way merge)
1517
1677
  \u26A0 conflicts: ${conflicts}. Resolve the git markers (<<<<<<< / >>>>>>>) by hand.`);
1518
1678
  }
1519
1679
  });
1520
- program.command("diff").description("Preview update changes (without writing)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").action((feature, opts) => {
1521
- console.log(renderPlan(diffFeatureCmd(feature, opts.registry)));
1680
+ 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) => {
1681
+ console.log(renderPlan(diffFeatureCmd(feature, opts.registry, opts.project)));
1522
1682
  });
1523
- program.command("list").description("Installed and available features").option("--registry <path>", "path to the registry").action((opts) => {
1524
- const { installed, available } = listFeatures(opts.registry);
1683
+ 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) => {
1684
+ const { installed, available } = listFeatures(opts.registry, opts.project);
1525
1685
  console.log("Installed:", installed.join(", ") || "\u2014");
1526
1686
  console.log("Available:", available.join(", ") || "\u2014");
1527
1687
  });
1528
1688
  var design = program.command("design").description("AI design step (wrapper over Claude Code)");
1529
- 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) => {
1530
- const code = designApplyCmd({ bin: opts.bin, dryRun: opts.dryRun });
1689
+ 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) => {
1690
+ const code = designApplyCmd({ bin: opts.bin, dryRun: opts.dryRun, projectRoot: opts.project });
1531
1691
  if (code !== 0) process.exit(code);
1532
1692
  });
1533
1693
  var kit = program.command("kit").description("Local tooling (the ~/.vitrine registry cache)");
@@ -1549,8 +1709,8 @@ program.command("self-update").description("Update the CLI itself (@vitrine-kit/
1549
1709
  const code = selfUpdate({ dryRun: opts.dryRun });
1550
1710
  if (code !== 0) process.exit(code);
1551
1711
  });
1552
- program.command("doctor").description("Check consistency: vitrine.json \u2194 files \u2194 packages \u2194 env").option("--registry <path>", "path to the registry").action((opts) => {
1553
- const report = doctorCmd(opts.registry);
1712
+ 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) => {
1713
+ const report = doctorCmd(opts.registry, opts.project);
1554
1714
  if (report.issues.length === 0) {
1555
1715
  console.log("\u2713 No issues found.");
1556
1716
  return;
@@ -1614,7 +1774,7 @@ program.command("init").description("Create a new client repository").argument("
1614
1774
  const finalTier = tier ?? "catalog";
1615
1775
  const backend = opts.backend ?? defaultBackend(finalTier);
1616
1776
  const finalFeatures = features ?? suggestFeatures(finalTier, registry, backend);
1617
- const root = resolve6(String(opts.dir ?? process.cwd()), name);
1777
+ const root = resolve7(String(opts.dir ?? process.cwd()), name);
1618
1778
  const res = initProject({ root, name, backend, tier: finalTier, features: finalFeatures, registry });
1619
1779
  console.log(`Created project "${name}" \u2192 ${root}`);
1620
1780
  console.log(`Installed: ${res.installed.join(", ") || "\u2014"}`);