@vitrine-kit/vitrine 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +18 -18
  2. package/dist/index.js +304 -285
  3. package/kit/registry/README.md +9 -0
  4. package/kit/registry/_index.json +74 -0
  5. package/kit/registry/cart/docs/cart.md +13 -0
  6. package/kit/registry/cart/feature.json +51 -0
  7. package/kit/registry/cart/files/app/(frontend)/cart/page.tsx +21 -0
  8. package/kit/registry/cart/files/app/api/cart/route.ts +41 -0
  9. package/kit/registry/cart/files/components/cart/AddToCart.tsx +40 -0
  10. package/kit/registry/cart/files/components/cart/CartIndicator.tsx +11 -0
  11. package/kit/registry/cart/files/components/cart/CartLineItem.tsx +27 -0
  12. package/kit/registry/cart/files/components/cart/CartSummary.tsx +31 -0
  13. package/kit/registry/cart/files/components/cart/CartView.tsx +31 -0
  14. package/kit/registry/cart/files/lib/cart/data.ts +12 -0
  15. package/kit/registry/cart/files/lib/cart/register.ts +10 -0
  16. package/kit/registry/catalog/docs/catalog.md +18 -0
  17. package/kit/registry/catalog/feature.json +43 -0
  18. package/kit/registry/catalog/files/components/catalog/CategoryNav.tsx +26 -0
  19. package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +42 -0
  20. package/kit/registry/catalog/files/components/catalog/ProductGrid.tsx +25 -0
  21. package/kit/registry/catalog/files/lib/catalog/data.ts +24 -0
  22. package/kit/registry/catalog/files/lib/catalog/register.ts +8 -0
  23. package/kit/registry/checkout/docs/checkout.md +17 -0
  24. package/kit/registry/checkout/feature.json +47 -0
  25. package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -0
  26. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +31 -0
  27. package/kit/registry/checkout/files/lib/checkout/fulfill.ts +72 -0
  28. package/kit/registry/checkout/files/lib/checkout/register.ts +8 -0
  29. package/kit/registry/checkout-paddle/docs/checkout-paddle.md +22 -0
  30. package/kit/registry/checkout-paddle/feature.json +62 -0
  31. package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +22 -0
  32. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +59 -0
  33. package/kit/registry/checkout-paddle/files/lib/checkout-paddle/register.ts +7 -0
  34. package/kit/registry/checkout-stripe/docs/checkout-stripe.md +17 -0
  35. package/kit/registry/checkout-stripe/feature.json +54 -0
  36. package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +23 -0
  37. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +73 -0
  38. package/kit/registry/checkout-stripe/files/lib/checkout-stripe/register.ts +7 -0
  39. package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +20 -0
  40. package/kit/registry/checkout-yookassa/feature.json +52 -0
  41. package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +22 -0
  42. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +82 -0
  43. package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/register.ts +7 -0
  44. package/kit/registry/product-page/docs/product-page.md +16 -0
  45. package/kit/registry/product-page/feature.json +39 -0
  46. package/kit/registry/product-page/files/components/product/ProductGallery.tsx +37 -0
  47. package/kit/registry/product-page/files/components/product/ProductView.tsx +42 -0
  48. package/kit/registry/product-page/files/lib/product/data.ts +10 -0
  49. package/kit/registry/seo/docs/seo.md +14 -0
  50. package/kit/registry/seo/feature.json +35 -0
  51. package/kit/registry/seo/files/components/seo/JsonLd.tsx +15 -0
  52. package/kit/registry/seo/files/lib/seo/jsonld.ts +29 -0
  53. package/kit/registry/seo/files/lib/seo/metadata.ts +35 -0
  54. package/kit/registry/stubs/route-glue.d.ts +45 -0
  55. package/kit/registry/tsconfig.json +21 -0
  56. package/kit/registry/tsconfig.routes.json +9 -0
  57. package/kit/templates/README.md +27 -0
  58. package/kit/templates/backend-payload/files/.dockerignore +8 -0
  59. package/kit/templates/backend-payload/files/Dockerfile +29 -0
  60. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/not-found.tsx +18 -0
  61. package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/page.tsx +18 -0
  62. package/kit/templates/backend-payload/files/app/(payload)/admin/importMap.js +3 -0
  63. package/kit/templates/backend-payload/files/app/(payload)/api/[...slug]/route.ts +18 -0
  64. package/kit/templates/backend-payload/files/app/(payload)/api/graphql/route.ts +6 -0
  65. package/kit/templates/backend-payload/files/app/(payload)/api/graphql-playground/route.ts +5 -0
  66. package/kit/templates/backend-payload/files/app/(payload)/custom.scss +1 -0
  67. package/kit/templates/backend-payload/files/app/(payload)/layout.tsx +23 -0
  68. package/kit/templates/backend-payload/files/docker-compose.yml +35 -0
  69. package/kit/templates/backend-payload/files/lib/adapter/db-decision.ts +43 -0
  70. package/kit/templates/backend-payload/files/lib/adapter/db-ping.ts +29 -0
  71. package/kit/templates/backend-payload/files/lib/adapter/db.ts +27 -0
  72. package/kit/templates/backend-payload/files/lib/adapter/index.ts +31 -0
  73. package/kit/templates/backend-payload/files/lib/adapter/map.ts +112 -0
  74. package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +79 -0
  75. package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +154 -0
  76. package/kit/templates/backend-payload/files/lib/adapter/payload-types.ts +50 -0
  77. package/kit/templates/backend-payload/files/lib/seed/admin.ts +23 -0
  78. package/kit/templates/backend-payload/files/lib/seed/demo.ts +79 -0
  79. package/kit/templates/backend-payload/files/lib/seed/guards.ts +5 -0
  80. package/kit/templates/backend-payload/files/lib/seed/richtext.ts +26 -0
  81. package/kit/templates/backend-payload/files/lib/seed/run.ts +51 -0
  82. package/kit/templates/backend-payload/files/next.config.mjs +10 -0
  83. package/kit/templates/backend-payload/files/payload.config.ts +32 -0
  84. package/kit/templates/backend-payload/files/seed-assets/placeholder-1.svg +5 -0
  85. package/kit/templates/backend-payload/files/seed-assets/placeholder-2.svg +5 -0
  86. package/kit/templates/backend-payload/files/seed-assets/placeholder-3.svg +5 -0
  87. package/kit/templates/backend-payload/files/seed-assets/placeholder-4.svg +5 -0
  88. package/kit/templates/backend-payload/files/seed-assets/placeholder-5.svg +5 -0
  89. package/kit/templates/backend-vendure/files/.dockerignore +8 -0
  90. package/kit/templates/backend-vendure/files/Dockerfile +24 -0
  91. package/kit/templates/backend-vendure/files/docker-compose.yml +50 -0
  92. package/kit/templates/backend-vendure/files/lib/adapter/graphql.ts +27 -0
  93. package/kit/templates/backend-vendure/files/lib/adapter/index.ts +20 -0
  94. package/kit/templates/backend-vendure/files/lib/adapter/map.ts +107 -0
  95. package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +62 -0
  96. package/kit/templates/backend-vendure/files/lib/adapter/vendure-commerce.ts +76 -0
  97. package/kit/templates/backend-vendure/files/lib/adapter/vendure-types.ts +61 -0
  98. package/kit/templates/backend-vendure/files/lib/seed/populate.ts +22 -0
  99. package/kit/templates/backend-vendure/files/src/index.ts +15 -0
  100. package/kit/templates/backend-vendure/files/vendure-config.ts +56 -0
  101. package/kit/templates/base/files/.claude/commands/add-feature.md +18 -0
  102. package/kit/templates/base/files/.claude/commands/design.md +14 -0
  103. package/kit/templates/base/files/.claude/commands/doctor.md +15 -0
  104. package/kit/templates/base/files/.claude/commands/setup.md +15 -0
  105. package/kit/templates/base/files/.claude/commands/update.md +16 -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 +7 -6
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ function preflightNode(min = 20) {
18
18
  const major = Number(process.versions.node.split(".")[0]);
19
19
  if (Number.isFinite(major) && major < min) {
20
20
  throw new Error(
21
- `[vitrine] \u043D\u0443\u0436\u0435\u043D Node >= ${min} (\u0442\u0435\u043A\u0443\u0449\u0438\u0439 ${process.versions.node}). \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 Node ${min} LTS: https://nodejs.org`
21
+ `[vitrine] Node >= ${min} required (current ${process.versions.node}). Install Node ${min} LTS: https://nodejs.org`
22
22
  );
23
23
  }
24
24
  }
@@ -46,7 +46,7 @@ function safeJoin(root, ...segs) {
46
46
  const target = resolve(base, ...segs);
47
47
  const rel = relative(base, target);
48
48
  if (rel.startsWith("..") || isAbsolute(rel)) {
49
- throw new Error(`[vitrine] \u043F\u0443\u0442\u044C "${join(...segs)}" \u0432\u044B\u0445\u043E\u0434\u0438\u0442 \u0437\u0430 \u043F\u0440\u0435\u0434\u0435\u043B\u044B "${root}"`);
49
+ throw new Error(`[vitrine] path "${join(...segs)}" escapes "${root}"`);
50
50
  }
51
51
  return target;
52
52
  }
@@ -84,7 +84,7 @@ function replaceBetween(content, startMarker, endMarker, replacement) {
84
84
  const si = content.indexOf(startMarker);
85
85
  const ei = content.indexOf(endMarker);
86
86
  if (si === -1 || ei === -1 || ei < si) {
87
- throw new Error(`[vitrine] \u043C\u0430\u0440\u043A\u0435\u0440\u044B "${startMarker}"/"${endMarker}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B`);
87
+ throw new Error(`[vitrine] markers "${startMarker}"/"${endMarker}" not found`);
88
88
  }
89
89
  const afterStartLine = content.indexOf("\n", si) + 1;
90
90
  const endLineStart = content.lastIndexOf("\n", ei) + 1;
@@ -121,25 +121,36 @@ function loadProject(root) {
121
121
  }
122
122
 
123
123
  // src/registry.ts
124
- import { existsSync as existsSync4 } from "fs";
125
- import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
124
+ import { existsSync as existsSync5 } from "fs";
125
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "path";
126
126
  import { featureManifestSchema } from "@vitrine-kit/contracts";
127
127
 
128
+ // src/bundled-kit.ts
129
+ import { existsSync as existsSync3 } from "fs";
130
+ import { dirname as dirname3, join as join3 } from "path";
131
+ import { fileURLToPath } from "url";
132
+ function bundledKitRoot() {
133
+ const distDir = dirname3(fileURLToPath(import.meta.url));
134
+ const candidate = join3(distDir, "..", "kit");
135
+ if (existsSync3(join3(candidate, "registry", "_index.json"))) return candidate;
136
+ return null;
137
+ }
138
+
128
139
  // src/cache.ts
129
- import { cpSync, existsSync as existsSync3, rmSync } from "fs";
130
- import { join as join3, resolve as resolve3 } from "path";
140
+ import { cpSync, existsSync as existsSync4, rmSync } from "fs";
141
+ import { join as join4, resolve as resolve3 } from "path";
131
142
  function vitrineHome() {
132
143
  if (process.env.VITRINE_HOME) return resolve3(process.env.VITRINE_HOME);
133
144
  const home = process.env.USERPROFILE ?? process.env.HOME;
134
- if (!home) throw new Error("[vitrine] \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u0434\u043E\u043C\u0430\u0448\u043D\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 (HOME/USERPROFILE)");
135
- return join3(home, ".vitrine");
145
+ if (!home) throw new Error("[vitrine] could not determine the home directory (HOME/USERPROFILE)");
146
+ return join4(home, ".vitrine");
136
147
  }
137
148
  function cachePaths(home = vitrineHome()) {
138
149
  return {
139
150
  root: home,
140
- registry: join3(home, "registry"),
141
- templates: join3(home, "templates"),
142
- meta: join3(home, "kit.json")
151
+ registry: join4(home, "registry"),
152
+ templates: join4(home, "templates"),
153
+ meta: join4(home, "kit.json")
143
154
  };
144
155
  }
145
156
  function readKitMeta(home = vitrineHome()) {
@@ -151,7 +162,7 @@ function writeKitMeta(home, meta) {
151
162
  `);
152
163
  }
153
164
  function readIndex(registryRoot) {
154
- const file = join3(registryRoot, "_index.json");
165
+ const file = join4(registryRoot, "_index.json");
155
166
  return exists(file) ? readJson(file) : null;
156
167
  }
157
168
  function computeChangelog(oldIndex, newIndex) {
@@ -171,7 +182,7 @@ function computeChangelog(oldIndex, newIndex) {
171
182
  return entries;
172
183
  }
173
184
  function formatChangelog(entries) {
174
- if (entries.length === 0) return "\u0431\u0435\u0437 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0430\u0431\u043E\u0440\u0430 \u0444\u0438\u0447";
185
+ if (entries.length === 0) return "no changes to the feature set";
175
186
  return entries.map((e) => {
176
187
  if (e.kind === "added") return `+ ${e.name}${e.to ? ` ${e.to}` : ""}`;
177
188
  if (e.kind === "removed") return `- ${e.name}`;
@@ -180,16 +191,16 @@ function formatChangelog(entries) {
180
191
  }
181
192
  function populateCache(fromDir, opts = {}) {
182
193
  const home = opts.home ?? vitrineHome();
183
- const srcRegistry = join3(fromDir, "registry");
184
- const srcTemplates = join3(fromDir, "templates");
185
- if (!existsSync3(join3(srcRegistry, "_index.json"))) {
186
- throw new Error(`[vitrine] \u0432 "${fromDir}" \u043D\u0435\u0442 registry/_index.json \u2014 \u044D\u0442\u043E \u043D\u0435 \u0434\u0435\u0440\u0435\u0432\u043E kit`);
194
+ const srcRegistry = join4(fromDir, "registry");
195
+ const srcTemplates = join4(fromDir, "templates");
196
+ if (!existsSync4(join4(srcRegistry, "_index.json"))) {
197
+ throw new Error(`[vitrine] "${fromDir}" has no registry/_index.json \u2014 not a kit tree`);
187
198
  }
188
199
  const paths = cachePaths(home);
189
200
  const oldIndex = readIndex(paths.registry);
190
201
  rmSync(paths.registry, { recursive: true, force: true });
191
202
  cpSync(srcRegistry, paths.registry, { recursive: true });
192
- if (existsSync3(srcTemplates)) {
203
+ if (existsSync4(srcTemplates)) {
193
204
  rmSync(paths.templates, { recursive: true, force: true });
194
205
  cpSync(srcTemplates, paths.templates, { recursive: true });
195
206
  }
@@ -207,9 +218,9 @@ function populateCache(fromDir, opts = {}) {
207
218
  function findUpRegistry(start) {
208
219
  let dir = resolve4(start);
209
220
  for (; ; ) {
210
- const candidate = join4(dir, "registry");
211
- if (existsSync4(join4(candidate, "_index.json"))) return candidate;
212
- const parent = dirname3(dir);
221
+ const candidate = join5(dir, "registry");
222
+ if (existsSync5(join5(candidate, "_index.json"))) return candidate;
223
+ const parent = dirname4(dir);
213
224
  if (parent === dir) return null;
214
225
  dir = parent;
215
226
  }
@@ -224,60 +235,62 @@ function resolveRegistryRoot(explicit) {
224
235
  home = null;
225
236
  }
226
237
  if (home) {
227
- const cache = join4(home, "registry");
228
- if (existsSync4(join4(cache, "_index.json"))) return cache;
238
+ const cache = join5(home, "registry");
239
+ if (existsSync5(join5(cache, "_index.json"))) return cache;
229
240
  }
230
241
  const dev = findUpRegistry(process.cwd());
231
242
  if (dev) return dev;
232
- throw new Error('[vitrine] \u0440\u0435\u0435\u0441\u0442\u0440 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D. \u0417\u0430\u043F\u0443\u0441\u0442\u0438 "vitrine kit update" \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438 --registry.');
243
+ const bundled = bundledKitRoot();
244
+ if (bundled) return join5(bundled, "registry");
245
+ throw new Error('[vitrine] registry not found. Run "vitrine kit update" or pass --registry.');
233
246
  }
234
247
  function createRegistrySource(explicitRoot) {
235
248
  const root = resolveRegistryRoot(explicitRoot);
236
249
  const cache = /* @__PURE__ */ new Map();
237
- const featureDir = (name) => join4(root, name);
238
- const hasFeature = (name) => existsSync4(join4(featureDir(name), "feature.json"));
250
+ const featureDir = (name) => join5(root, name);
251
+ const hasFeature = (name) => existsSync5(join5(featureDir(name), "feature.json"));
239
252
  const loadManifest = (name) => {
240
253
  const cached = cache.get(name);
241
254
  if (cached) return cached;
242
- const file = join4(featureDir(name), "feature.json");
243
- if (!existsSync4(file)) throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435`);
255
+ const file = join5(featureDir(name), "feature.json");
256
+ if (!existsSync5(file)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
244
257
  const manifest = featureManifestSchema.parse(JSON.parse(readText(file)));
245
258
  cache.set(name, manifest);
246
259
  return manifest;
247
260
  };
248
261
  const listFeatures2 = () => {
249
- const index = JSON.parse(readText(join4(root, "_index.json")));
262
+ const index = JSON.parse(readText(join5(root, "_index.json")));
250
263
  return Object.keys(index.features ?? {});
251
264
  };
252
265
  return { root, featureDir, hasFeature, loadManifest, listFeatures: listFeatures2 };
253
266
  }
254
267
 
255
268
  // src/install.ts
256
- import { join as join6 } from "path";
269
+ import { join as join7 } from "path";
257
270
 
258
271
  // src/transaction.ts
259
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
260
- import { dirname as dirname4 } from "path";
272
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
273
+ import { dirname as dirname5 } from "path";
261
274
  var FsTransaction = class {
262
275
  backups = [];
263
276
  snapshot(path) {
264
277
  if (this.backups.some((b) => b.path === path)) return;
265
- if (existsSync5(path)) this.backups.push({ path, existed: true, prev: readFileSync2(path, "utf8") });
278
+ if (existsSync6(path)) this.backups.push({ path, existed: true, prev: readFileSync2(path, "utf8") });
266
279
  else this.backups.push({ path, existed: false });
267
280
  }
268
281
  write(path, content) {
269
282
  this.snapshot(path);
270
- mkdirSync2(dirname4(path), { recursive: true });
283
+ mkdirSync2(dirname5(path), { recursive: true });
271
284
  writeFileSync2(path, content, "utf8");
272
285
  }
273
286
  remove(path) {
274
287
  this.snapshot(path);
275
- if (existsSync5(path)) rmSync2(path);
288
+ if (existsSync6(path)) rmSync2(path);
276
289
  }
277
290
  rollback() {
278
291
  for (const b of [...this.backups].reverse()) {
279
292
  if (b.existed) writeFileSync2(b.path, b.prev, "utf8");
280
- else if (existsSync5(b.path)) rmSync2(b.path);
293
+ else if (existsSync6(b.path)) rmSync2(b.path);
281
294
  }
282
295
  this.backups = [];
283
296
  }
@@ -326,7 +339,7 @@ function assertNoPascalCollisions(features) {
326
339
  const id = pascalCase(f.name);
327
340
  const prev = byPascal.get(id);
328
341
  if (prev && prev !== f.name) {
329
- throw new Error(`[vitrine] \u0444\u0438\u0447\u0438 "${prev}" \u0438 "${f.name}" \u0434\u0430\u044E\u0442 \u043E\u0434\u0438\u043D \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 "${id}" \u2014 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u0443\u0439\u0442\u0435 \u043E\u0434\u043D\u0443`);
342
+ throw new Error(`[vitrine] features "${prev}" and "${f.name}" produce the same identifier "${id}" \u2014 rename one`);
330
343
  }
331
344
  byPascal.set(id, f.name);
332
345
  }
@@ -339,11 +352,13 @@ function renderSlotsFile(features) {
339
352
  );
340
353
  const calls = withSlots.map((f) => ` register${pascalCase(f.name)}Slots();`);
341
354
  return [
342
- "// vitrine:generated \u2014 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F \u0441\u043B\u043E\u0442\u043E\u0432 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0444\u0438\u0447. \u041D\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.",
355
+ "// vitrine:generated \u2014 slot registration for installed features. Do not edit by hand.",
356
+ "import { slotRegistry } from '@vitrine-kit/core';",
343
357
  ...imports,
344
358
  "",
345
359
  "export function registerSlots(): void {",
346
- ...calls.length ? calls : [" // \u043D\u0435\u0442 \u0444\u0438\u0447 \u0441\u043E \u0441\u043B\u043E\u0442\u0430\u043C\u0438"],
360
+ " slotRegistry.clear();",
361
+ ...calls.length ? calls : [" // no slot features"],
347
362
  "}",
348
363
  ""
349
364
  ].join("\n");
@@ -356,11 +371,13 @@ function renderPaymentsFile(features) {
356
371
  );
357
372
  const calls = withPayment.map((f) => ` register${pascalCase(f.name)}Provider();`);
358
373
  return [
359
- "// vitrine:generated \u2014 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F \u043F\u043B\u0430\u0442\u0451\u0436\u043D\u044B\u0445 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u043E\u0432 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0444\u0438\u0447. \u041D\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.",
374
+ "// vitrine:generated \u2014 payment-provider registration for installed features. Do not edit by hand.",
375
+ "import { payments } from '@vitrine-kit/core';",
360
376
  ...imports,
361
377
  "",
362
378
  "export function registerPayments(): void {",
363
- ...calls.length ? calls : [" // \u043D\u0435\u0442 \u043F\u043B\u0430\u0442\u0451\u0436\u043D\u044B\u0445 \u0444\u0438\u0447"],
379
+ " payments.clear();",
380
+ ...calls.length ? calls : [" // no payment features"],
364
381
  "}",
365
382
  ""
366
383
  ].join("\n");
@@ -373,7 +390,7 @@ function renderBlueprintFile(features) {
373
390
  );
374
391
  const calls = withBp.map((f) => ` extend${pascalCase(f.name)}Blueprint(blueprint);`);
375
392
  return [
376
- "// vitrine:generated \u2014 blueprint \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0444\u0438\u0447. \u041D\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.",
393
+ "// vitrine:generated \u2014 blueprint of installed features. Do not edit by hand.",
377
394
  "import { createBlueprint } from '@vitrine-kit/payload-blueprint';",
378
395
  ...imports,
379
396
  "",
@@ -385,9 +402,9 @@ function renderBlueprintFile(features) {
385
402
  ].join("\n");
386
403
  }
387
404
  function renderClaudeFeaturesTable(features) {
388
- if (features.length === 0) return "_\u0424\u0438\u0447\u0438 \u0435\u0449\u0451 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B._";
405
+ if (features.length === 0) return "_No features installed yet._";
389
406
  const rows = features.slice().sort((a, b) => a.name.localeCompare(b.name)).map((f) => `| \`${f.name}\` | ${f.manifest.title} | ${f.version} |`).join("\n");
390
- return ["| \u0424\u0438\u0447\u0430 | \u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 | \u0412\u0435\u0440\u0441\u0438\u044F |", "|---|---|---|", rows].join("\n");
407
+ return ["| Feature | Description | Version |", "|---|---|---|", rows].join("\n");
391
408
  }
392
409
  function mergeEnvExample(existing, features) {
393
410
  const present = parseEnvKeys(existing);
@@ -439,7 +456,7 @@ function renderNeutralTheme() {
439
456
  };
440
457
  const lines = TOKEN_CSS_VARS.map((v) => ` ${v}: ${neutral[v] ?? "initial"};`).join("\n");
441
458
  return [
442
- "/* vitrine: \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u043A\u043B\u0438\u0435\u043D\u0442\u0430. \u0414\u0438\u0437\u0430\u0439\u043D-\u0448\u0430\u0433 (vitrine design apply) \u043F\u0435\u0440\u0435\u043F\u0438\u0441\u044B\u0432\u0430\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u044D\u0442\u043E. */",
459
+ "/* vitrine: client token values. The design step (vitrine design apply) rewrites only this. */",
443
460
  ":root {",
444
461
  lines,
445
462
  "}",
@@ -448,14 +465,14 @@ function renderNeutralTheme() {
448
465
  }
449
466
 
450
467
  // src/feature-files.ts
451
- import { join as join5 } from "path";
468
+ import { join as join6 } from "path";
452
469
  function* eachFeatureFile(featDir, map) {
453
- const src = join5(featDir, map.from);
470
+ const src = join6(featDir, map.from);
454
471
  if (!exists(src)) return;
455
472
  const rels = isDir(src) ? walkRelFiles(src) : [""];
456
473
  for (const rel of rels) {
457
- const srcAbs = rel ? join5(src, rel) : src;
458
- const repoRel = rel ? join5(map.to, rel) : map.to;
474
+ const srcAbs = rel ? join6(src, rel) : src;
475
+ const repoRel = rel ? join6(map.to, rel) : map.to;
459
476
  yield { srcAbs, repoRel, toRel: toPosix(repoRel) };
460
477
  }
461
478
  }
@@ -467,7 +484,7 @@ function resolveOrder(names, registry) {
467
484
  const visit = (name) => {
468
485
  if (seen.has(name)) return;
469
486
  if (!registry.hasFeature(name)) {
470
- throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435`);
487
+ throw new Error(`[vitrine] feature "${name}" not found in the registry`);
471
488
  }
472
489
  seen.add(name);
473
490
  const manifest = registry.loadManifest(name);
@@ -480,21 +497,21 @@ function resolveOrder(names, registry) {
480
497
  function validate(name, manifest, project) {
481
498
  if (!manifest.tier.includes(project.lock.tier)) {
482
499
  throw new Error(
483
- `[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0443\u0440\u043E\u0432\u0435\u043D\u044C "${project.lock.tier}" (\u0442\u043E\u043B\u044C\u043A\u043E ${manifest.tier.join(", ")})`
500
+ `[vitrine] feature "${name}" does not support tier "${project.lock.tier}" (only ${manifest.tier.join(", ")})`
484
501
  );
485
502
  }
486
503
  for (const conflict of manifest.conflicts ?? []) {
487
504
  if (project.lock.features[conflict]) {
488
- throw new Error(`[vitrine] \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442: "${name}" \u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u0430 \u0441 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0439 "${conflict}"`);
505
+ throw new Error(`[vitrine] conflict: "${name}" is incompatible with the installed "${conflict}"`);
489
506
  }
490
507
  }
491
508
  }
492
509
  function copyFeatureFiles(project, name, manifest, registry, tx) {
493
510
  const featDir = registry.featureDir(name);
494
- const originalsBase = join6(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
511
+ const originalsBase = join7(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
495
512
  for (const map of manifest.files) {
496
- if (!exists(join6(featDir, map.from))) {
497
- throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}": \u043D\u0435\u0442 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 "${map.from}"`);
513
+ if (!exists(join7(featDir, map.from))) {
514
+ throw new Error(`[vitrine] feature "${name}": no source "${map.from}"`);
498
515
  }
499
516
  for (const file of eachFeatureFile(featDir, map)) {
500
517
  const content = readText(file.srcAbs);
@@ -581,19 +598,19 @@ function installFeatures(project, names, registry) {
581
598
  }
582
599
  function removeFeature(project, name, registry) {
583
600
  const removed = project.lock.features[name];
584
- if (!removed) throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430`);
601
+ if (!removed) throw new Error(`[vitrine] feature "${name}" is not installed`);
585
602
  const manifest = registry.loadManifest(name);
586
603
  if (!manifest.removable) {
587
- throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u0443\u0434\u0430\u043B\u044F\u0435\u043C\u0430 (removable: false)`);
604
+ throw new Error(`[vitrine] feature "${name}" is not removable (removable: false)`);
588
605
  }
589
606
  for (const other of Object.keys(project.lock.features)) {
590
607
  if (other === name) continue;
591
608
  const deps = registry.loadManifest(other).registryDependencies ?? [];
592
609
  if (deps.includes(name)) {
593
- throw new Error(`[vitrine] \u043D\u0435\u043B\u044C\u0437\u044F \u0443\u0434\u0430\u043B\u0438\u0442\u044C "${name}": \u043E\u0442 \u043D\u0435\u0451 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 "${other}"`);
610
+ throw new Error(`[vitrine] cannot remove "${name}": "${other}" depends on it`);
594
611
  }
595
612
  }
596
- const originalsDir = join6(projectPaths(project.root).originals, `${name}@${removed.version}`);
613
+ const originalsDir = join7(projectPaths(project.root).originals, `${name}@${removed.version}`);
597
614
  const featDir = registry.featureDir(name);
598
615
  const targets = /* @__PURE__ */ new Set();
599
616
  if (isDir(originalsDir)) for (const rel of walkRelFiles(originalsDir)) targets.add(rel);
@@ -616,30 +633,30 @@ function removeFeature(project, name, registry) {
616
633
 
617
634
  // src/design.ts
618
635
  import { spawnSync } from "child_process";
619
- import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
620
- import { delimiter, join as join7 } from "path";
636
+ import { existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
637
+ import { delimiter, join as join8 } from "path";
621
638
  import { TOKEN_CSS_VARS as TOKEN_CSS_VARS2 } from "@vitrine-kit/contracts";
622
- var DESIGN_HEADING = "## \u0418\u041D\u0421\u0422\u0420\u0423\u041A\u0426\u0418\u042F: \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D \u0438\u0437 /design";
639
+ var DESIGN_HEADING = "## INSTRUCTION: apply the design from /design";
623
640
  function findClaudeBin(explicit) {
624
641
  const pinned = explicit ?? process.env.VITRINE_CLAUDE_BIN;
625
642
  if (pinned) {
626
- if (existsSync6(pinned)) return pinned;
627
- throw new Error(`[vitrine] Claude Code \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043F\u043E \u043F\u0443\u0442\u0438 "${pinned}"`);
643
+ if (existsSync7(pinned)) return pinned;
644
+ throw new Error(`[vitrine] Claude Code not found at path "${pinned}"`);
628
645
  }
629
646
  const names = process.platform === "win32" ? ["claude.cmd", "claude.exe", "claude.ps1", "claude"] : ["claude"];
630
647
  for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
631
648
  for (const name of names) {
632
- const full = join7(dir, name);
633
- if (existsSync6(full)) return full;
649
+ const full = join8(dir, name);
650
+ if (existsSync7(full)) return full;
634
651
  }
635
652
  }
636
653
  throw new Error(
637
- "[vitrine] Claude Code CLI \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 PATH. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0435\u0433\u043E (npm i -g @anthropic-ai/claude-code) \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0443\u0442\u044C \u0447\u0435\u0440\u0435\u0437 --bin / VITRINE_CLAUDE_BIN. design apply \u2014 \u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u043D\u0430\u0434 Claude Code, \u0441\u0432\u043E\u0435\u0433\u043E \u043A\u043B\u044E\u0447\u0430 Anthropic \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442."
654
+ "[vitrine] Claude Code CLI not found in PATH. Install it (npm i -g @anthropic-ai/claude-code) or pass the path via --bin / VITRINE_CLAUDE_BIN. design apply is a wrapper over Claude Code; it needs no Anthropic key of its own."
638
655
  );
639
656
  }
640
657
  function designHasInput(root) {
641
- const dir = join7(root, "design");
642
- if (!existsSync6(dir)) return false;
658
+ const dir = join8(root, "design");
659
+ if (!existsSync7(dir)) return false;
643
660
  return readdirSync2(dir).some((entry) => !/^readme\.md$/i.test(entry));
644
661
  }
645
662
  function extractDesignInstruction(claudeMd) {
@@ -649,20 +666,20 @@ function extractDesignInstruction(claudeMd) {
649
666
  return claudeMd.slice(start, next === -1 ? void 0 : next).trim();
650
667
  }
651
668
  function buildDesignPrompt(project) {
652
- const claudeMd = existsSync6(join7(project.root, "CLAUDE.md")) ? readText(join7(project.root, "CLAUDE.md")) : "";
669
+ const claudeMd = existsSync7(join8(project.root, "CLAUDE.md")) ? readText(join8(project.root, "CLAUDE.md")) : "";
653
670
  const instruction = extractDesignInstruction(claudeMd) ?? DESIGN_HEADING;
654
671
  const tokens = TOKEN_CSS_VARS2.map((v) => ` ${v}`).join("\n");
655
672
  return [
656
673
  instruction,
657
674
  "",
658
- "\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u0438\u044F:",
659
- "- \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0434\u0438\u0437\u0430\u0439\u043D\u0430: \u043F\u0430\u043F\u043A\u0430 `design/` (\u044D\u043A\u0441\u043F\u043E\u0440\u0442 \u0438\u0437 Claude Design).",
660
- "- \u0415\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0435\u043C\u044B\u0439 \u0444\u0430\u0439\u043B: `theme/client.css` \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F CSS-\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0445.",
661
- "- \u0417\u0430\u043C\u043A\u043D\u0443\u0442\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u0438\u043C\u0451\u043D \u0442\u043E\u043A\u0435\u043D\u043E\u0432 (\u0434\u0440\u0443\u0433\u0438\u0445 \u043D\u0435 \u0432\u0432\u043E\u0434\u0438\u0442\u044C):",
675
+ "Application context:",
676
+ "- Design source: the `design/` folder (export from Claude Design).",
677
+ "- The only editable file: `theme/client.css` \u2014 set the CSS variable values.",
678
+ "- Closed set of token names (do not introduce others):",
662
679
  tokens,
663
680
  "",
664
- "\u041D\u0415 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C: \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u044B, \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u044B, \u0440\u043E\u0443\u0442\u044B, site.config, lib/*. \u0422\u043E\u043B\u044C\u043A\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F",
665
- "\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0445 \u0432 theme/client.css. \u0418\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C: \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u043D\u0430\u043A\u0430\u043F\u043B\u0438\u0432\u0430\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F."
681
+ "Do NOT edit: components, adapters, routes, site.config, lib/*. Only the variable",
682
+ "values in theme/client.css. Idempotency: re-running does not accumulate changes."
666
683
  ].join("\n");
667
684
  }
668
685
  var defaultRunner = ({ bin, args, cwd }) => {
@@ -679,19 +696,19 @@ function planDesignApply(project, opts = {}) {
679
696
  function designApply(project, opts = {}, runner = defaultRunner) {
680
697
  if (!designHasInput(project.root)) {
681
698
  throw new Error(
682
- "[vitrine] \u043F\u0430\u043F\u043A\u0430 design/ \u043F\u0443\u0441\u0442\u0430 \u2014 \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435 \u044D\u043A\u0441\u043F\u043E\u0440\u0442 \u0438\u0437 Claude Design \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435."
699
+ "[vitrine] the design/ folder is empty \u2014 add an export from Claude Design and retry."
683
700
  );
684
701
  }
685
702
  const cmd = planDesignApply(project, opts);
686
703
  if (opts.dryRun) {
687
- console.log(`[vitrine] dry-run: ${cmd.bin} -p <\u043F\u0440\u043E\u043C\u043F\u0442 ${cmd.prompt.length} \u0441\u0438\u043C\u0432.> --permission-mode acceptEdits`);
704
+ console.log(`[vitrine] dry-run: ${cmd.bin} -p <prompt ${cmd.prompt.length} chars> --permission-mode acceptEdits`);
688
705
  return 0;
689
706
  }
690
707
  return runner(cmd);
691
708
  }
692
709
 
693
710
  // src/doctor.ts
694
- import { join as join8 } from "path";
711
+ import { join as join9 } from "path";
695
712
  function runDoctor(project, registry) {
696
713
  const paths = projectPaths(project.root);
697
714
  const issues = [];
@@ -704,21 +721,21 @@ function runDoctor(project, registry) {
704
721
  const paymentsText = exists(paths.payments) ? readText(paths.payments) : "";
705
722
  for (const core of ["@vitrine-kit/contracts", "@vitrine-kit/core"]) {
706
723
  if (!deps[core]) {
707
- add({ severity: "error", scope: "packages", message: `\u043D\u0435\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 ${core}`, fix: "\u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0432 package.json" });
724
+ add({ severity: "error", scope: "packages", message: `missing dependency ${core}`, fix: "add it to package.json" });
708
725
  }
709
726
  }
710
- if (exists(paths.claude) && !readText(paths.claude).includes("\u0418\u041D\u0421\u0422\u0420\u0423\u041A\u0426\u0418\u042F: \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D")) {
727
+ if (exists(paths.claude) && !readText(paths.claude).includes("INSTRUCTION: apply the design")) {
711
728
  add({
712
729
  severity: "warn",
713
730
  scope: "design",
714
- message: "\u0432 CLAUDE.md \u043D\u0435\u0442 \u0431\u043B\u043E\u043A\u0430 \u0434\u0438\u0437\u0430\u0439\u043D-\u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u0438",
715
- fix: "\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 CLAUDE.md (kit update \u043F\u0440\u0438\u043D\u043E\u0441\u0438\u0442 \u0441\u0432\u0435\u0436\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E)"
731
+ message: "CLAUDE.md has no design-instruction block",
732
+ fix: "update CLAUDE.md (kit update brings a fresh instruction)"
716
733
  });
717
734
  }
718
735
  for (const [name, pin] of Object.entries(project.lock.features)) {
719
736
  const scope = `feature:${name}`;
720
737
  if (!registry.hasFeature(name)) {
721
- add({ severity: "error", scope, message: `\u0444\u0438\u0447\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435`, fix: "vitrine kit update" });
738
+ add({ severity: "error", scope, message: `feature not found in the registry`, fix: "vitrine kit update" });
722
739
  continue;
723
740
  }
724
741
  const manifest = registry.loadManifest(name);
@@ -726,19 +743,19 @@ function runDoctor(project, registry) {
726
743
  add({
727
744
  severity: "warn",
728
745
  scope,
729
- message: `\u0432\u0435\u0440\u0441\u0438\u044F \u0432 \u0440\u0435\u043F\u043E ${pin.version}, \u0440\u0435\u0435\u0441\u0442\u0440 \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u0442 ${manifest.kitVersion}`,
746
+ message: `repo version ${pin.version}, registry offers ${manifest.kitVersion}`,
730
747
  fix: `vitrine update ${name}`
731
748
  });
732
749
  }
733
750
  const featDir = registry.featureDir(name);
734
751
  for (const map of manifest.files) {
735
752
  for (const file of eachFeatureFile(featDir, map)) {
736
- if (!exists(join8(project.root, file.repoRel))) {
753
+ if (!exists(join9(project.root, file.repoRel))) {
737
754
  add({
738
755
  severity: "error",
739
756
  scope,
740
- message: `\u043D\u0435\u0442 \u0444\u0430\u0439\u043B\u0430 "${file.toRel}"`,
741
- fix: `vitrine add ${name} (\u043F\u0435\u0440\u0435\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442)`
757
+ message: `missing file "${file.toRel}"`,
758
+ fix: `vitrine add ${name} (reinstalls)`
742
759
  });
743
760
  }
744
761
  }
@@ -748,33 +765,33 @@ function runDoctor(project, registry) {
748
765
  add({
749
766
  severity: e.required ? "error" : "warn",
750
767
  scope,
751
- message: `\u043D\u0435\u0442 \u043A\u043B\u044E\u0447\u0430 env "${e.key}"${e.required ? " (\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D)" : ""}`,
752
- fix: "\u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0432 .env.example/.env"
768
+ message: `missing env key "${e.key}"${e.required ? " (required)" : ""}`,
769
+ fix: "add it to .env.example/.env"
753
770
  });
754
771
  }
755
772
  }
756
773
  const need = [...Object.keys(manifest.corePackages ?? {}), ...(manifest.npm ?? []).map((s) => parseNpmSpec(s).name)];
757
774
  for (const dep of need) {
758
775
  if (!deps[dep]) {
759
- add({ severity: "error", scope, message: `\u043D\u0435\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 ${dep}`, fix: `vitrine add ${name} (\u0434\u043E\u043C\u0435\u0440\u0436\u0438\u0442 deps)` });
776
+ add({ severity: "error", scope, message: `missing dependency ${dep}`, fix: `vitrine add ${name} (merges deps)` });
760
777
  }
761
778
  }
762
779
  if ((manifest.slots?.length ?? 0) > 0) {
763
780
  const fn = `register${pascalCase(name)}Slots`;
764
781
  if (!slotsText.includes(fn)) {
765
- add({ severity: "error", scope, message: `lib/slots.ts \u043D\u0435 \u0432\u044B\u0437\u044B\u0432\u0430\u0435\u0442 ${fn}`, fix: `vitrine add ${name} (\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 slots)` });
782
+ add({ severity: "error", scope, message: `lib/slots.ts does not call ${fn}`, fix: `vitrine add ${name} (regenerates slots)` });
766
783
  }
767
784
  }
768
785
  if (!configText.includes(`"${name}": true`)) {
769
- add({ severity: "warn", scope, message: `\u0432 site.config \u043D\u0435\u0442 \u0444\u043B\u0430\u0433\u0430 features.${name}`, fix: `vitrine add ${name} (\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0444\u043B\u0430\u0433\u0438)` });
786
+ add({ severity: "warn", scope, message: `site.config has no features.${name} flag`, fix: `vitrine add ${name} (regenerates flags)` });
770
787
  }
771
788
  if (manifest.payment) {
772
789
  const fn = `register${pascalCase(name)}Provider`;
773
790
  if (!paymentsText.includes(fn)) {
774
- add({ severity: "error", scope, message: `lib/payments.ts \u043D\u0435 \u0432\u044B\u0437\u044B\u0432\u0430\u0435\u0442 ${fn}`, fix: `vitrine add ${name} (\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 payments)` });
791
+ add({ severity: "error", scope, message: `lib/payments.ts does not call ${fn}`, fix: `vitrine add ${name} (regenerates payments)` });
775
792
  }
776
793
  if (!configText.includes(`payments: ${JSON.stringify(manifest.payment.provider)}`)) {
777
- add({ severity: "warn", scope, message: `\u0432 site.config integrations.payments \u2260 "${manifest.payment.provider}"`, fix: `vitrine add ${name} (\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 integrations)` });
794
+ add({ severity: "warn", scope, message: `site.config integrations.payments \u2260 "${manifest.payment.provider}"`, fix: `vitrine add ${name} (regenerates integrations)` });
778
795
  }
779
796
  }
780
797
  }
@@ -783,7 +800,7 @@ function runDoctor(project, registry) {
783
800
 
784
801
  // src/update.ts
785
802
  import { rmSync as rmSync3 } from "fs";
786
- import { join as join9 } from "path";
803
+ import { join as join10 } from "path";
787
804
 
788
805
  // src/merge.ts
789
806
  function splitLines(s) {
@@ -851,8 +868,8 @@ function merge3(base, ours, theirs, labels = {}) {
851
868
  const B = splitLines(base);
852
869
  const O = splitLines(ours);
853
870
  const T = splitLines(theirs);
854
- const ourLabel = labels.ours ?? "ours (\u0440\u0435\u043F\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430)";
855
- const theirLabel = labels.theirs ?? "theirs (\u0440\u0435\u0435\u0441\u0442\u0440)";
871
+ const ourLabel = labels.ours ?? "ours (client repo)";
872
+ const theirLabel = labels.theirs ?? "theirs (registry)";
856
873
  if (B.length * Math.max(O.length, T.length) > MAX_LCS_CELLS) {
857
874
  if (ours === theirs || theirs === base) return { text: ours, clean: true, conflicts: 0 };
858
875
  if (ours === base) return { text: theirs, clean: true, conflicts: 0 };
@@ -921,19 +938,19 @@ function merge3(base, ours, theirs, labels = {}) {
921
938
  // src/update.ts
922
939
  function planUpdate(project, name, registry) {
923
940
  const pin = project.lock.features[name];
924
- if (!pin) throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430`);
925
- if (!registry.hasFeature(name)) throw new Error(`[vitrine] \u0444\u0438\u0447\u0430 "${name}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435`);
941
+ if (!pin) throw new Error(`[vitrine] feature "${name}" is not installed`);
942
+ if (!registry.hasFeature(name)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
926
943
  const manifest = registry.loadManifest(name);
927
944
  const fromVersion = pin.version;
928
945
  const toVersion = manifest.kitVersion;
929
946
  const featDir = registry.featureDir(name);
930
- const originalsBase = join9(projectPaths(project.root).originals, `${name}@${fromVersion}`);
947
+ const originalsBase = join10(projectPaths(project.root).originals, `${name}@${fromVersion}`);
931
948
  const files = [];
932
949
  for (const map of manifest.files) {
933
950
  for (const file of eachFeatureFile(featDir, map)) {
934
951
  const theirs = readText(file.srcAbs);
935
- const oursPath = join9(project.root, file.repoRel);
936
- const basePath = join9(originalsBase, file.repoRel);
952
+ const oursPath = join10(project.root, file.repoRel);
953
+ const basePath = join10(originalsBase, file.repoRel);
937
954
  const ours = exists(oursPath) ? readText(oursPath) : null;
938
955
  const base = exists(basePath) ? readText(basePath) : null;
939
956
  if (ours === null) {
@@ -961,8 +978,8 @@ function applyUpdate(project, plan, registry) {
961
978
  const manifest = registry.loadManifest(plan.feature);
962
979
  const featDir = registry.featureDir(plan.feature);
963
980
  const paths = projectPaths(project.root);
964
- const newOriginals = join9(paths.originals, `${plan.feature}@${plan.toVersion}`);
965
- const oldOriginals = join9(paths.originals, `${plan.feature}@${plan.fromVersion}`);
981
+ const newOriginals = join10(paths.originals, `${plan.feature}@${plan.toVersion}`);
982
+ const oldOriginals = join10(paths.originals, `${plan.feature}@${plan.fromVersion}`);
966
983
  const tx = new FsTransaction();
967
984
  try {
968
985
  for (const f of plan.files) {
@@ -987,17 +1004,17 @@ function applyUpdate(project, plan, registry) {
987
1004
  function renderPlan(plan) {
988
1005
  const head = plan.fromVersion === plan.toVersion ? `${plan.feature} @ ${plan.toVersion}` : `${plan.feature} ${plan.fromVersion} \u2192 ${plan.toVersion}`;
989
1006
  const lines = plan.files.filter((f) => f.status !== "unchanged").map((f) => {
990
- const mark = f.status === "conflict" ? "\u2717 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442" : f.status === "new" ? "+ \u043D\u043E\u0432\u044B\u0439 " : "~ \u0441\u043B\u0438\u044F\u043D\u0438\u0435";
991
- return ` ${mark} ${f.to}${f.conflicts ? ` (\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432: ${f.conflicts})` : ""}`;
1007
+ const mark = f.status === "conflict" ? "\u2717 conflict" : f.status === "new" ? "+ new " : "~ merge ";
1008
+ return ` ${mark} ${f.to}${f.conflicts ? ` (conflicts: ${f.conflicts})` : ""}`;
992
1009
  });
993
- return lines.length === 0 ? `${head}: \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435\u0442` : [head, ...lines].join("\n");
1010
+ return lines.length === 0 ? `${head}: no changes` : [head, ...lines].join("\n");
994
1011
  }
995
1012
 
996
1013
  // src/commands.ts
997
1014
  function requireProject() {
998
1015
  const root = findProjectRoot();
999
1016
  if (!root) {
1000
- throw new Error("[vitrine] \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D vitrine.json \u2014 \u044D\u0442\u043E \u043D\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442\u0430 Vitrine");
1017
+ throw new Error("[vitrine] vitrine.json not found \u2014 not a Vitrine client repository");
1001
1018
  }
1002
1019
  return loadProject(root);
1003
1020
  }
@@ -1037,32 +1054,34 @@ function diffFeatureCmd(name, registryRoot) {
1037
1054
 
1038
1055
  // src/init.ts
1039
1056
  import { readdirSync as readdirSync3 } from "fs";
1040
- import { join as join11 } from "path";
1057
+ import { join as join12 } from "path";
1041
1058
 
1042
- // src/kit.ts
1043
- var KIT_VERSION = "0.0.0";
1059
+ // src/kit-versions.generated.ts
1060
+ var KIT_VERSION = "0.4.0";
1044
1061
  var CONTRACTS_VERSION = "1.0.0";
1045
1062
  var CONTRACTS_RANGE = "^1.0.0";
1046
- var CORE_RANGE = "^0.1.0";
1047
- var BLUEPRINT_RANGE = "^0.1.0";
1063
+ var CORE_RANGE = "^0.2.0";
1064
+ var BLUEPRINT_RANGE = "^0.2.0";
1065
+
1066
+ // src/kit.ts
1048
1067
  var REACT_RANGE = "^18.3.1";
1049
1068
  var CLIENT_REACT_RANGE = "^19.0.0";
1050
1069
  var NEXT_RANGE = "^15.1.0";
1051
1070
  var PAYLOAD_RANGE = "^3.0.0";
1052
1071
 
1053
1072
  // src/templates.ts
1054
- import { existsSync as existsSync7 } from "fs";
1055
- import { dirname as dirname5, join as join10 } from "path";
1073
+ import { existsSync as existsSync8 } from "fs";
1074
+ import { dirname as dirname6, join as join11 } from "path";
1056
1075
  function templatesRoot(registryRoot) {
1057
- return join10(dirname5(registryRoot), "templates");
1076
+ return join11(dirname6(registryRoot), "templates");
1058
1077
  }
1059
1078
  function hasTemplate(root, name) {
1060
- return existsSync7(join10(root, name, "files"));
1079
+ return existsSync8(join11(root, name, "files"));
1061
1080
  }
1062
1081
  function copyTemplate(root, name, destRoot) {
1063
- const filesDir = join10(root, name, "files");
1082
+ const filesDir = join11(root, name, "files");
1064
1083
  const rels = walkRelFiles(filesDir);
1065
- for (const rel of rels) writeText(join10(destRoot, rel), readText(join10(filesDir, rel)));
1084
+ for (const rel of rels) writeText(join11(destRoot, rel), readText(join11(filesDir, rel)));
1066
1085
  return rels;
1067
1086
  }
1068
1087
 
@@ -1077,7 +1096,7 @@ var PAYMENT_PROVIDER_FEATURES = [
1077
1096
  ];
1078
1097
  function suggestFeatures(tier, registry, backend = defaultBackend(tier)) {
1079
1098
  const core = ["catalog", "product-page", "seo"];
1080
- const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe", "reviews"];
1099
+ const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe"];
1081
1100
  const desired = tier === "catalog" ? core : [...core, ...shop];
1082
1101
  return desired.filter((name) => registry.hasFeature(name));
1083
1102
  }
@@ -1160,44 +1179,44 @@ function clientPackageJson(name, backend) {
1160
1179
  function clientEnvExample(backend) {
1161
1180
  if (backend === "vendure") {
1162
1181
  return [
1163
- "# \u041E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.",
1182
+ "# Project environment.",
1164
1183
  "",
1165
- "# \u0411\u0414. \u041F\u0443\u0441\u0442\u043E \u0432 dev \u2014 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 SQLite (.vitrine/vendure.sqlite).",
1184
+ "# DB. Empty in dev \u2014 built-in SQLite (.vitrine/vendure.sqlite).",
1166
1185
  "DATABASE_URL=",
1167
1186
  "",
1168
- "# Vendure Shop API (\u0432\u0438\u0442\u0440\u0438\u043D\u0430 \u2192 \u0441\u0435\u0440\u0432\u0435\u0440).",
1187
+ "# Vendure Shop API (storefront \u2192 server).",
1169
1188
  "VENDURE_SHOP_API_URL=http://localhost:3001/shop-api",
1170
1189
  "",
1171
- "# \u0421\u0443\u043F\u0435\u0440\u0430\u0434\u043C\u0438\u043D Vendure (dev-\u0434\u0435\u0444\u043E\u043B\u0442 superadmin/superadmin; \u0441\u043C\u0435\u043D\u0438\u0442\u0435 \u0434\u043B\u044F \u043F\u0440\u043E\u0434\u0430).",
1190
+ "# Vendure superadmin (dev default superadmin/superadmin; change it for prod).",
1172
1191
  "VENDURE_SUPERADMIN_USERNAME=",
1173
1192
  "VENDURE_SUPERADMIN_PASSWORD=",
1174
1193
  "VENDURE_COOKIE_SECRET=",
1175
1194
  "",
1176
- "# \u0411\u0430\u0437\u043E\u0432\u044B\u0439 URL \u0432\u0438\u0442\u0440\u0438\u043D\u044B.",
1195
+ "# Storefront base URL.",
1177
1196
  "NEXT_PUBLIC_SITE_URL=http://localhost:3000",
1178
1197
  ""
1179
1198
  ].join("\n");
1180
1199
  }
1181
1200
  if (backend !== "payload") {
1182
- return "# \u041E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.\nDATABASE_URL=\n";
1201
+ return "# Project environment.\nDATABASE_URL=\n";
1183
1202
  }
1184
1203
  return [
1185
- "# \u041E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.",
1204
+ "# Project environment.",
1186
1205
  "",
1187
- "# \u0411\u0414. \u0414\u043B\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0433\u043E dev \u043C\u043E\u0436\u043D\u043E \u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C \u2014 \u0431\u0443\u0434\u0435\u0442 SQLite-fallback (.vitrine/dev.sqlite).",
1206
+ "# DB. For local dev you can leave it empty \u2014 a SQLite fallback is used (.vitrine/dev.sqlite).",
1188
1207
  "DATABASE_URL=",
1189
1208
  "",
1190
- "# \u0421\u0435\u043A\u0440\u0435\u0442 Payload (\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D; \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0439 \u0434\u043B\u044F \u043F\u0440\u043E\u0434\u0430).",
1209
+ "# Payload secret (required; generate a random one for prod).",
1191
1210
  "PAYLOAD_SECRET=",
1192
1211
  "",
1193
- "# Dev-\u0430\u0434\u043C\u0438\u043D (\u0441\u043E\u0437\u0434\u0430\u0451\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0432 dev \u043F\u0440\u0438 \u043F\u0443\u0441\u0442\u043E\u0439 \u0411\u0414; \u043F\u0430\u0440\u043E\u043B\u044C \u043F\u0435\u0447\u0430\u0442\u0430\u0435\u0442\u0441\u044F \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u044C).",
1212
+ "# Dev admin (created only in dev when the DB is empty; the password is printed to the console).",
1194
1213
  "DEV_ADMIN_EMAIL=",
1195
1214
  "DEV_ADMIN_PASSWORD=",
1196
1215
  "",
1197
- "# \u0411\u0430\u0437\u043E\u0432\u044B\u0439 URL \u0441\u0430\u0439\u0442\u0430 (canonical, OG).",
1216
+ "# Site base URL (canonical, OG).",
1198
1217
  "NEXT_PUBLIC_SITE_URL=http://localhost:3000",
1199
1218
  "",
1200
- "# \u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C SQLite-fallback \u0434\u0430\u0436\u0435 \u0432 dev (\u043B\u043E\u0432\u0438\u0442\u044C \u043E\u043F\u0435\u0447\u0430\u0442\u043A\u0438 \u043A\u043E\u043D\u0444\u0438\u0433\u0430):",
1219
+ "# Disable the SQLite fallback even in dev (to catch config typos):",
1201
1220
  "# VITRINE_DB_STRICT=1",
1202
1221
  ""
1203
1222
  ].join("\n");
@@ -1207,13 +1226,13 @@ function clientReadme(name, backend, tier) {
1207
1226
  "```bash",
1208
1227
  "pnpm install",
1209
1228
  "cp .env.example .env",
1210
- "pnpm vendure # Vendure-\u0441\u0435\u0440\u0432\u0435\u0440 (Shop API \u043D\u0430 :3001) \u2014 \u0432 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u043C \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435",
1211
- "pnpm dev # \u0432\u0438\u0442\u0440\u0438\u043D\u0430 \u043D\u0430 :3000",
1229
+ "pnpm vendure # Vendure server (Shop API on :3001) \u2014 in a separate terminal",
1230
+ "pnpm dev # storefront on :3000",
1212
1231
  "```",
1213
1232
  "",
1214
- "\u0411\u0435\u0437 Postgres dev \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 SQLite (`.vitrine/vendure.sqlite`) \u0438",
1215
- "populate-\u0441\u0438\u0434. \u0421\u0443\u043F\u0435\u0440\u0430\u0434\u043C\u0438\u043D \u2014 \u0438\u0437 `VENDURE_SUPERADMIN_*` (dev-\u0434\u0435\u0444\u043E\u043B\u0442",
1216
- "superadmin/superadmin; \u0441\u043C\u0435\u043D\u0438\u0442\u0435 \u0434\u043B\u044F \u043F\u0440\u043E\u0434\u0430)."
1233
+ "Without Postgres, dev starts a built-in SQLite (`.vitrine/vendure.sqlite`) and",
1234
+ "the populate seed. The superadmin comes from `VENDURE_SUPERADMIN_*` (dev default",
1235
+ "superadmin/superadmin; change it for prod)."
1217
1236
  ].join("\n") : [
1218
1237
  "```bash",
1219
1238
  "pnpm install",
@@ -1221,69 +1240,69 @@ function clientReadme(name, backend, tier) {
1221
1240
  "pnpm dev",
1222
1241
  "```",
1223
1242
  "",
1224
- "- \u0412\u0438\u0442\u0440\u0438\u043D\u0430: http://localhost:3000",
1225
- "- \u0410\u0434\u043C\u0438\u043D\u043A\u0430: http://localhost:3000/admin",
1243
+ "- Storefront: http://localhost:3000",
1244
+ "- Admin: http://localhost:3000/admin",
1226
1245
  "",
1227
- "\u0411\u0435\u0437 Postgres dev \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 SQLite (`.vitrine/dev.sqlite`), \u0437\u0430\u043F\u043E\u043B\u043D\u044F\u0435\u0442",
1228
- "\u0434\u0435\u043C\u043E-\u043A\u0430\u0442\u0430\u043B\u043E\u0433 (5 \u0442\u043E\u0432\u0430\u0440\u043E\u0432, 2 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438) \u0438 \u0437\u0430\u0432\u043E\u0434\u0438\u0442 dev-\u0430\u0434\u043C\u0438\u043D\u0430 (\u043B\u043E\u0433\u0438\u043D/\u043F\u0430\u0440\u043E\u043B\u044C \u043F\u0435\u0447\u0430\u0442\u0430\u044E\u0442\u0441\u044F",
1229
- "\u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u044C \u043E\u0434\u0438\u043D \u0440\u0430\u0437). \u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C fallback \u0438 \u0432 dev \u2014 `VITRINE_DB_STRICT=1`."
1246
+ "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`."
1230
1249
  ].join("\n");
1231
- const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... # \u0441\u0435\u043A\u0440\u0435\u0442 cookie Vendure" : "export PAYLOAD_SECRET=... # \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0439 \u0441\u0435\u043A\u0440\u0435\u0442 Payload";
1250
+ const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... # Vendure cookie secret" : "export PAYLOAD_SECRET=... # random Payload secret";
1232
1251
  return `# ${name}
1233
1252
 
1234
- \u041A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0430 Vitrine. Backend: \`${backend}\`, \u0443\u0440\u043E\u0432\u0435\u043D\u044C: \`${tier}\`.
1235
- Next.js + Tailwind; \u0444\u0438\u0447\u0438 \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u044B \u0438\u0437 \u0440\u0435\u0435\u0441\u0442\u0440\u0430 Vitrine \u2014 \u0432\u044B \u0432\u043B\u0430\u0434\u0435\u0435\u0442\u0435 \u043A\u043E\u0434\u043E\u043C \u0438
1236
- \u0441\u0442\u0438\u043B\u0438\u0437\u0443\u0435\u0442\u0435 \u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430\u043C\u0438 (\`theme/client.css\`), \u043D\u0435 \u043C\u0435\u043D\u044F\u044F \u043B\u043E\u0433\u0438\u043A\u0443.
1253
+ A client project on Vitrine. Backend: \`${backend}\`, tier: \`${tier}\`.
1254
+ Next.js + Tailwind; features are copied from the Vitrine registry \u2014 you own the code and
1255
+ style it with tokens (\`theme/client.css\`) without changing the logic.
1237
1256
 
1238
- ## 1. \u041F\u0440\u0435\u0434\u0443\u0441\u043B\u043E\u0432\u0438\u044F
1257
+ ## 1. Prerequisites
1239
1258
 
1240
- Node >= 20 (LTS) \u0438 \`pnpm\`. \u041F\u0430\u043A\u0435\u0442\u044B \`@vitrine-kit/*\` \u043F\u0443\u0431\u043B\u0438\u0447\u043D\u044B \u0432 npm \u2014
1241
- \u0442\u043E\u043A\u0435\u043D \u0434\u043B\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.
1259
+ Node >= 20 (LTS) and \`pnpm\`. The \`@vitrine-kit/*\` packages are public on npm \u2014
1260
+ no token is needed to install them.
1242
1261
 
1243
- ## 2. \u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0437\u0430\u043F\u0443\u0441\u043A (zero-config)
1262
+ ## 2. Local run (zero-config)
1244
1263
 
1245
1264
  ${run}
1246
1265
 
1247
- ## 3. \u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D \u043A\u043B\u0438\u0435\u043D\u0442\u0430
1266
+ ## 3. Apply the client design
1248
1267
 
1249
- 1. \u041F\u043E\u043B\u043E\u0436\u0438\u0442\u0435 \u044D\u043A\u0441\u043F\u043E\u0440\u0442 \u0431\u0440\u0435\u043D\u0434\u0430 (Figma export, \u0441\u043A\u0440\u0438\u043D\u0448\u043E\u0442\u044B, \u0430\u0441\u0441\u0435\u0442\u044B) \u0432 \`/design\`.
1250
- 2. \`vitrine design apply\` \u2014 \u0418\u0418 \u0437\u0430\u0434\u0430\u0451\u0442 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0432 \`theme/client.css\`
1251
- (\u043B\u043E\u0433\u0438\u043A\u0443/\u0434\u0430\u043D\u043D\u044B\u0435/\u0440\u043E\u0443\u0442\u0438\u043D\u0433/a11y \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442). \u0428\u0430\u0433 \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u0435\u043D.
1268
+ 1. Put the brand export (Figma export, screenshots, assets) in \`/design\`.
1269
+ 2. \`vitrine design apply\` \u2014 the AI sets token values in \`theme/client.css\`
1270
+ (doesn't touch logic/data/routing/a11y). The step is idempotent.
1252
1271
 
1253
- ## 4. \u0424\u0438\u0447\u0438: \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C, \u0443\u0431\u0440\u0430\u0442\u044C, \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C
1272
+ ## 4. Features: add, remove, view
1254
1273
 
1255
1274
  \`\`\`bash
1256
- vitrine list # \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0435 + \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435
1257
- vitrine add reviews # \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0438\u0447\u0443: \u0444\u043B\u0430\u0433, \u0441\u043B\u043E\u0442\u044B, blueprint, env
1258
- vitrine remove reviews # \u0443\u0431\u0440\u0430\u0442\u044C (\u0435\u0441\u043B\u0438 \u0444\u0438\u0447\u0430 removable)
1259
- vitrine design apply # \u0441\u0442\u0438\u043B\u0438\u0437\u043E\u0432\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0444\u0438\u0447\u0443
1275
+ vitrine list # installed + available
1276
+ vitrine add cart # copy a feature: flag, slots, blueprint, env
1277
+ vitrine remove cart # remove (if the feature is removable)
1278
+ vitrine design apply # style the new feature
1260
1279
  \`\`\`
1261
1280
 
1262
- \`add\` \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u0435\u043D \u0438 \u0442\u0440\u0430\u043D\u0437\u0430\u043A\u0446\u0438\u043E\u043D\u0435\u043D (\u043E\u0442\u043A\u0430\u0442 \u043F\u0440\u0438 \u043E\u0448\u0438\u0431\u043A\u0435); \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u044B \u0432\u0435\u0440\u0441\u0438\u0439 \u043F\u0438\u0448\u0443\u0442\u0441\u044F \u0432
1263
- \`.vitrine/originals/\` \u2014 \u043E\u0441\u043D\u043E\u0432\u0430 \u0434\u043B\u044F 3-way merge \u043F\u0440\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0438.
1281
+ \`add\` is idempotent and transactional (rollback on error); version originals are written to
1282
+ \`.vitrine/originals/\` \u2014 the basis for 3-way merge on update.
1264
1283
 
1265
- ## 5. \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430
1284
+ ## 5. Updates and checks
1266
1285
 
1267
1286
  \`\`\`bash
1268
- vitrine kit update # \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u044D\u0448 \u0440\u0435\u0435\u0441\u0442\u0440\u0430/\u0448\u0430\u0431\u043B\u043E\u043D\u043E\u0432 \u0441 GitHub
1269
- vitrine diff <feature> # \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0444\u0438\u0447\u0438
1270
- vitrine update [feature] # 3-way merge \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u0444\u0438\u0447\u0438 (\u0431\u0430\u0437\u0430 = \u0432\u0430\u0448 \u0441\u043D\u0430\u043F\u0448\u043E\u0442)
1271
- vitrine doctor # \u043A\u043E\u043D\u0441\u0438\u0441\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C vitrine.json \u2194 \u0444\u0430\u0439\u043B\u044B \u2194 \u043F\u0430\u043A\u0435\u0442\u044B \u2194 env
1287
+ vitrine kit update # update the local registry/templates cache from GitHub
1288
+ vitrine diff <feature> # preview a feature update
1289
+ vitrine update [feature] # 3-way merge of the new feature version (base = your snapshot)
1290
+ vitrine doctor # consistency: vitrine.json \u2194 files \u2194 packages \u2194 env
1272
1291
  \`\`\`
1273
1292
 
1274
- \u041F\u0430\u043A\u0435\u0442\u044B \`@vitrine-kit/*\` \u0432\u0435\u0440\u0441\u0438\u043E\u043D\u0438\u0440\u0443\u044E\u0442\u0441\u044F \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E: \u0444\u0438\u043A\u0441 \u0432 \`core\` \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E
1275
- \`@vitrine-kit/core\`, \u0430 \`@vitrine-kit/contracts\` \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u043D\u0430 \u0441\u0432\u043E\u0435\u0439 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u2014
1276
- \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0439\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0432 \`package.json\` \u0442\u043E\u0447\u0435\u0447\u043D\u043E.
1293
+ The \`@vitrine-kit/*\` packages are versioned independently: a fix in \`core\` bumps only
1294
+ \`@vitrine-kit/core\`, while \`@vitrine-kit/contracts\` stays at its stable version \u2014
1295
+ update versions in \`package.json\` selectively.
1277
1296
 
1278
- ## 6. \u0414\u0435\u043F\u043B\u043E\u0439 (VPS + Docker)
1297
+ ## 6. Deploy (VPS + Docker)
1279
1298
 
1280
1299
  \`\`\`bash
1281
1300
  ${deploySecret}
1282
1301
  docker compose up --build
1283
1302
  \`\`\`
1284
1303
 
1285
- Production \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0435\u0430\u043B\u044C\u043D\u044B\u0439 \`DATABASE_URL\` \u2014 \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442 \u043F\u0440\u0435\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F
1286
- (SQLite-fallback \u0442\u043E\u043B\u044C\u043A\u043E \u0432 dev).
1304
+ Production requires a real \`DATABASE_URL\` \u2014 without it the start aborts
1305
+ (the SQLite fallback is dev-only).
1287
1306
  `;
1288
1307
  }
1289
1308
  function scaffoldBase(opts) {
@@ -1294,10 +1313,10 @@ function scaffoldBase(opts) {
1294
1313
  const backendTemplate = `backend-${backend}`;
1295
1314
  if (hasTemplate(tRoot, backendTemplate)) copyTemplate(tRoot, backendTemplate, root);
1296
1315
  if (!baseCopied) {
1297
- writeText(join11(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1316
+ writeText(join12(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
1298
1317
  }
1299
1318
  writeText(
1300
- join11(root, "vitrine.json"),
1319
+ join12(root, "vitrine.json"),
1301
1320
  `${JSON.stringify(
1302
1321
  { kitVersion: KIT_VERSION, contracts: CONTRACTS_VERSION, backend, tier, features: {} },
1303
1322
  null,
@@ -1306,7 +1325,7 @@ function scaffoldBase(opts) {
1306
1325
  `
1307
1326
  );
1308
1327
  writeText(
1309
- join11(root, "site.config.ts"),
1328
+ join12(root, "site.config.ts"),
1310
1329
  `import type { SiteConfig } from '@vitrine-kit/contracts';
1311
1330
 
1312
1331
  export const siteConfig: SiteConfig = {
@@ -1320,90 +1339,90 @@ export const siteConfig: SiteConfig = {
1320
1339
  // vitrine:integrations:start
1321
1340
  integrations: {},
1322
1341
  // vitrine:integrations:end
1323
- i18n: { defaultLocale: 'ru', locales: ['ru'], currency: 'RUB' },
1342
+ i18n: { defaultLocale: 'en', locales: ['en'], currency: 'USD' },
1324
1343
  };
1325
1344
 
1326
1345
  export default siteConfig;
1327
1346
  `
1328
1347
  );
1329
1348
  writeText(
1330
- join11(root, "CLAUDE.md"),
1349
+ join12(root, "CLAUDE.md"),
1331
1350
  `# ${name}
1332
1351
 
1333
- \u041F\u0440\u043E\u0435\u043A\u0442 \u043D\u0430 Vitrine. Backend: \`${backend}\`, \u0443\u0440\u043E\u0432\u0435\u043D\u044C: \`${tier}\`.
1334
- \u042D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B \u2014 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0439 \u0433\u0430\u0439\u0434 \u0434\u043B\u044F \u0418\u0418-\u0430\u0433\u0435\u043D\u0442\u0430 (Claude Code) \u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A\u0430. \u0412\u0441\u0435 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u043E
1335
- \u0441\u0442\u0430\u0440\u0442\u0435\u0440-\u043A\u0438\u0442\u043E\u043C \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u044E\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 CLI \`vitrine\`; \u0433\u043E\u0442\u043E\u0432\u044B\u0435 \u043F\u043E\u0442\u043E\u043A\u0438 \u2014 \u0441\u043B\u044D\u0448-\u043A\u043E\u043C\u0430\u043D\u0434\u0430\u043C\u0438 \u0432 \`.claude/commands/\`.
1352
+ A Vitrine project. Backend: \`${backend}\`, tier: \`${tier}\`.
1353
+ This file is the operational guide for the AI agent (Claude Code) and the developer. All
1354
+ starter-kit operations go through the \`vitrine\` CLI; ready-made flows are the slash commands in \`.claude/commands/\`.
1336
1355
 
1337
- ## \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0444\u0438\u0447\u0438
1356
+ ## Installed features
1338
1357
  <!-- vitrine:features:start -->
1339
- _\u0424\u0438\u0447\u0438 \u0435\u0449\u0451 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B._
1358
+ _No features installed yet._
1340
1359
  <!-- vitrine:features:end -->
1341
1360
 
1342
- ## \u041A\u043E\u043C\u0430\u043D\u0434\u044B vitrine CLI
1361
+ ## vitrine CLI commands
1343
1362
 
1344
- | \u041A\u043E\u043C\u0430\u043D\u0434\u0430 | \u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 | \u041A\u043E\u0433\u0434\u0430 \u0437\u0432\u0430\u0442\u044C | \u0424\u043B\u0430\u0433\u0438 |
1363
+ | Command | Purpose | When to use | Flags |
1345
1364
  |---|---|---|---|
1346
- | \`vitrine list\` | \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u0444\u0438\u0447\u0438 | \u043F\u0435\u0440\u0435\u0434 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u0444\u0438\u0447\u0438 | \u2014 |
1347
- | \`vitrine add <features\u2026>\` | \u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0438\u0447\u0443(\u0438): \u0444\u0430\u0439\u043B\u044B, \u0444\u043B\u0430\u0433, \u0441\u043B\u043E\u0442\u044B, blueprint, env, deps | \xAB\u0434\u043E\u0431\u0430\u0432\u044C \u0444\u0438\u0447\u0443 X\xBB | \`--registry\` |
1348
- | \`vitrine remove <feature>\` | \u0423\u0431\u0440\u0430\u0442\u044C \u0444\u0438\u0447\u0443 (\u0435\u0441\u043B\u0438 \`removable\`) | \xAB\u0443\u0431\u0435\u0440\u0438 \u0444\u0438\u0447\u0443 X\xBB | \`--registry\` |
1349
- | \`vitrine update [features\u2026]\` | \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0444\u0438\u0447\u0438 3-way merge (\u0431\u0435\u0437 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u2014 \u0432\u0441\u0435) | \u043F\u043E\u0441\u043B\u0435 \`kit update\` | \`--dry-run\`, \`--registry\` |
1350
- | \`vitrine diff <feature>\` | \u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F (\u0431\u0435\u0437 \u0437\u0430\u043F\u0438\u0441\u0438) | \u043F\u0435\u0440\u0435\u0434 \`update\` | \`--registry\` |
1351
- | \`vitrine doctor\` | \u041A\u043E\u043D\u0441\u0438\u0441\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C: \`vitrine.json\` \u2194 \u0444\u0430\u0439\u043B\u044B \u2194 \u043F\u0430\u043A\u0435\u0442\u044B \u2194 env | \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u0430\u0432\u043E\u043A, \u043F\u0440\u0438 \u0441\u043E\u043C\u043D\u0435\u043D\u0438\u044F\u0445 | \`--registry\` |
1352
- | \`vitrine design apply\` | \u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D \u0438\u0437 \`/design\` \u043A \u0442\u043E\u043A\u0435\u043D\u0430\u043C (\u0447\u0435\u0440\u0435\u0437 Claude Code) | \u043F\u043E\u0441\u043B\u0435 \`add\` \u0438\u043B\u0438 \u0441\u043C\u0435\u043D\u044B \u0431\u0440\u0435\u043D\u0434\u0430 | \`--bin\`, \`--dry-run\` |
1353
- | \`vitrine kit update\` | \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u044D\u0448 \u0440\u0435\u0435\u0441\u0442\u0440\u0430/\u0448\u0430\u0431\u043B\u043E\u043D\u043E\u0432 \u0441 GitHub | \u043F\u0435\u0440\u0435\u0434 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u0444\u0438\u0447 | \`--from\`, \`--version\`, \`--channel\` |
1354
- | \`vitrine kit status\` | \u0412\u0435\u0440\u0441\u0438\u044F \u043A\u044D\u0448\u0430 vs \u043E\u0436\u0438\u0434\u0430\u0435\u043C\u0430\u044F CLI | \u0434\u0438\u0430\u0433\u043D\u043E\u0441\u0442\u0438\u043A\u0430 | \u2014 |
1355
- | \`vitrine self-update\` | \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0430\u043C CLI | \u0440\u0435\u0434\u043A\u043E | \`--dry-run\` |
1365
+ | \`vitrine list\` | Installed and available features | before adding a feature | \u2014 |
1366
+ | \`vitrine add <features\u2026>\` | Copy feature(s): files, flag, slots, blueprint, env, deps | "add feature X" | \`--registry\` |
1367
+ | \`vitrine remove <feature>\` | Remove a feature (if \`removable\`) | "remove feature X" | \`--registry\` |
1368
+ | \`vitrine update [features\u2026]\` | Update features via 3-way merge (all if no arguments) | after \`kit update\` | \`--dry-run\`, \`--registry\` |
1369
+ | \`vitrine diff <feature>\` | Preview an update (without writing) | before \`update\` | \`--registry\` |
1370
+ | \`vitrine doctor\` | Consistency: \`vitrine.json\` \u2194 files \u2194 packages \u2194 env | after edits, when in doubt | \`--registry\` |
1371
+ | \`vitrine design apply\` | Apply the design from \`/design\` to tokens (via Claude Code) | after \`add\` or a rebrand | \`--bin\`, \`--dry-run\` |
1372
+ | \`vitrine kit update\` | Update the local registry/templates cache from GitHub | before updating features | \`--from\`, \`--version\`, \`--channel\` |
1373
+ | \`vitrine kit status\` | Cache version vs the CLI's expected one | diagnostics | \u2014 |
1374
+ | \`vitrine self-update\` | Update the CLI itself | rarely | \`--dry-run\` |
1356
1375
 
1357
- \`init\` \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\u0432\u0438\u0437\u0430\u0440\u0434 \`vitrine init\`). \`add\`/\`update\`
1358
- \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u044B \u0438 \u0442\u0440\u0430\u043D\u0437\u0430\u043A\u0446\u0438\u043E\u043D\u043D\u044B (\u043E\u0442\u043A\u0430\u0442 \u043F\u0440\u0438 \u043E\u0448\u0438\u0431\u043A\u0435); \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u044B \u0432\u0435\u0440\u0441\u0438\u0439 \u043F\u0438\u0448\u0443\u0442\u0441\u044F \u0432 \`.vitrine/originals/\`
1359
- \u2014 \u0431\u0430\u0437\u0430 \u0434\u043B\u044F 3-way merge.
1376
+ \`init\` runs once when the repository is created (the \`vitrine init\` wizard). \`add\`/\`update\`
1377
+ are idempotent and transactional (rollback on error); version originals are written to \`.vitrine/originals/\`
1378
+ \u2014 the base for 3-way merge.
1360
1379
 
1361
- ## \u0422\u0438\u043F\u043E\u0432\u044B\u0435 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0438
1380
+ ## Common scenarios
1362
1381
 
1363
- - **\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430** \u2192 \`/setup\`: \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438, GitHub PAT, \`.env\`, \u0437\u0430\u043F\u0443\u0441\u043A dev-\u0441\u0435\u0440\u0432\u0435\u0440\u0430.
1364
- - **\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0438 \u0441\u0442\u0438\u043B\u0438\u0437\u043E\u0432\u0430\u0442\u044C \u0444\u0438\u0447\u0443** \u2192 \`/add-feature <\u0438\u043C\u044F>\`: \`list\` \u2192 \`add\` \u2192 \`design apply\` \u2192 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430.
1365
- - **\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C/\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D** \u2192 \`/design\`: \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u044C \u044D\u043A\u0441\u043F\u043E\u0440\u0442 \u0432 \`/design\`, \`design apply\`.
1366
- - **\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0444\u0438\u0447\u0438** \u2192 \`/update\`: \`kit update\` \u2192 \`diff\` \u2192 \`update\` \u2192 \u0440\u0430\u0437\u0440\u0443\u043B\u0438\u0442\u044C \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u044B \u2192 \`doctor\`.
1367
- - **\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043A\u043E\u043D\u0441\u0438\u0441\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C** \u2192 \`/doctor\`.
1382
+ - **Project setup** \u2192 \`/setup\`: dependencies, \`.env\`, start the dev server.
1383
+ - **Add and style a feature** \u2192 \`/add-feature <name>\`: \`list\` \u2192 \`add\` \u2192 \`design apply\` \u2192 check.
1384
+ - **Apply/update the design** \u2192 \`/design\`: put the export in \`/design\`, \`design apply\`.
1385
+ - **Update features** \u2192 \`/update\`: \`kit update\` \u2192 \`diff\` \u2192 \`update\` \u2192 resolve conflicts \u2192 \`doctor\`.
1386
+ - **Check consistency** \u2192 \`/doctor\`.
1368
1387
 
1369
- \u041F\u043E\u043B\u043D\u044B\u0439 \u0433\u0430\u0439\u0434 \u0434\u043B\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u2014 \u0432 \`README.md\`.
1388
+ The full human guide is in \`README.md\`.
1370
1389
 
1371
- ## \u0418\u041D\u0421\u0422\u0420\u0423\u041A\u0426\u0418\u042F: \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D \u0438\u0437 /design
1372
- \u0412\u0445\u043E\u0434: \u0432\u0441\u0451 \u0432 \`/design\`.
1373
- \u0417\u0430\u0434\u0430\u0447\u0430: \u0438\u0437\u0432\u043B\u0435\u0447\u044C \u0432\u0438\u0437\u0443\u0430\u043B\u044C\u043D\u044B\u0439 \u044F\u0437\u044B\u043A (\u043F\u0430\u043B\u0438\u0442\u0440\u0430, \u0442\u0438\u043F\u043E\u0433\u0440\u0430\u0444\u0438\u043A\u0430, \u043E\u0442\u0441\u0442\u0443\u043F\u044B, \u0440\u0430\u0434\u0438\u0443\u0441\u044B, \u0442\u0435\u043D\u0438,
1374
- \u0432\u0438\u0434 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0445 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u043E\u0432) \u0438 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u043A \u043F\u0440\u043E\u0435\u043A\u0442\u0443.
1375
- \u041F\u0440\u0438\u043C\u0435\u043D\u044F\u0442\u044C \u0442\u0430\u043A:
1376
- 1) \u0437\u0430\u0434\u0430\u0442\u044C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0432 \`theme/client.css\` \u2014 \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0440\u044B\u0447\u0430\u0433;
1377
- 2) \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0442\u043E\u043A\u0435\u043D \u043D\u0435 \u0432\u044B\u0440\u0430\u0436\u0430\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0435 \u2014 \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0437\u0435\u043D\u0442\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u043A\u043B\u0430\u0441\u0441\u044B
1378
- \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u043C\u0443 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u0443, \u041D\u0415 \u043C\u0435\u043D\u044F\u044F \u0435\u0433\u043E \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443.
1379
- \u041D\u0415 \u043C\u0435\u043D\u044F\u0442\u044C: \u043B\u043E\u0433\u0438\u043A\u0443 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u043E\u0432, \u0432\u044B\u0431\u043E\u0440\u043A\u0443 \u0434\u0430\u043D\u043D\u044B\u0445, \u0432\u044B\u0437\u043E\u0432\u044B \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u0430, \u0440\u043E\u0443\u0442\u0438\u043D\u0433,
1380
- a11y-\u0440\u043E\u043B\u0438/\u043B\u0435\u0439\u0431\u043B\u044B, \u043F\u0443\u0431\u043B\u0438\u0447\u043D\u044B\u0435 \u043F\u0440\u043E\u043F\u0441\u044B. \u0422\u043E\u043A\u0435\u043D\u044B \u2014 \u044D\u0442\u043E \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441.
1381
- \u0415\u0441\u043B\u0438 \u0434\u0438\u0437\u0430\u0439\u043D \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0438\u043D\u043E\u0439 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B \u0441\u0435\u043A\u0446\u0438\u0438 \u2014 \u0441\u043E\u0437\u0434\u0430\u0442\u044C override \u0441\u0435\u043A\u0446\u0438\u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
1382
- (\u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F), \u0430 \u043D\u0435 \u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043E\u0431\u0449\u0438\u0439 wireframe.
1383
- \u0428\u0430\u0433 \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u0435\u043D: \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F, \u043D\u0435 \u043D\u0430\u043A\u0430\u043F\u043B\u0438\u0432\u0430\u0435\u0442 \u043C\u0443\u0441\u043E\u0440.
1390
+ ## INSTRUCTION: apply the design from /design
1391
+ Input: everything in \`/design\`.
1392
+ Task: extract the visual language (palette, typography, spacing, radii, shadows,
1393
+ the look of specific components) and apply it to the project.
1394
+ Apply it like this:
1395
+ 1) set token values in \`theme/client.css\` \u2014 the main lever;
1396
+ 2) only if a token can't express what's needed \u2014 add presentational classes
1397
+ to a specific component WITHOUT changing its structure.
1398
+ Do NOT change: component logic, data fetching, adapter calls, routing,
1399
+ a11y roles/labels, public props. Tokens are the interface.
1400
+ If the design requires a different section structure \u2014 create a section override in the repo
1401
+ (composition) rather than editing the shared wireframe.
1402
+ The step is idempotent: re-running converges and doesn't accumulate cruft.
1384
1403
 
1385
- ## \u0413\u0440\u0430\u043D\u0438\u0446\u044B (\u0447\u0442\u043E \u0430\u0433\u0435\u043D\u0442\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u0442\u0440\u043E\u0433\u0430\u0442\u044C)
1386
- - **\u0413\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C\u044B\u0435/\u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u2014 \u0440\u0443\u043A\u0430\u043C\u0438 \u043D\u0435 \u043F\u0440\u0430\u0432\u0438\u0442\u044C** (CLI \u043F\u0435\u0440\u0435\u0437\u0430\u0442\u0440\u0451\u0442 \u0438\u0437 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F):
1387
- \`lib/slots.ts\`, \`lib/payments.ts\`, \`lib/blueprint.ts\`, \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C\u044B\u0435 \u0440\u0435\u0433\u0438\u043E\u043D\u044B \`site.config.ts\`
1388
- (\`features\`/\`integrations\`), \`vitrine.json\`, \u0442\u0430\u0431\u043B\u0438\u0446\u0430 \u0444\u0438\u0447 \u0432 \u044D\u0442\u043E\u043C \`CLAUDE.md\`, \`.env*\`.
1389
- \u041D\u0430\u0431\u043E\u0440 \u0444\u0438\u0447/\u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 \`vitrine add/remove\`, \u0430 \u043D\u0435 \u043F\u0440\u0430\u0432\u043A\u043E\u0439 \u0444\u0430\u0439\u043B\u043E\u0432.
1390
- - **\u0414\u0438\u0437\u0430\u0439\u043D \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0442\u043E\u043A\u0435\u043D\u043E\u0432** \u0432 \`theme/client.css\` (\u0447\u0435\u0440\u0435\u0437 \`vitrine design apply\`):
1391
- \u043B\u043E\u0433\u0438\u043A\u0443/\u0434\u0430\u043D\u043D\u044B\u0435/\u0440\u043E\u0443\u0442\u0438\u043D\u0433/a11y/\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u043E\u0432 \u043D\u0435 \u043C\u0435\u043D\u044F\u0442\u044C.
1392
- - **\u041A\u043E\u043D\u0442\u0440\u0430\u043A\u0442\u044B \u0440\u0430\u0441\u0448\u0438\u0440\u044F\u044E\u0442\u0441\u044F \u0430\u0434\u0434\u0438\u0442\u0438\u0432\u043D\u043E** (\`@vitrine-kit/contracts\`): \u043B\u043E\u043C\u0430\u0442\u044C \u0444\u043E\u0440\u043C\u0443 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0445 \u043F\u043E\u043B\u0435\u0439 \u043D\u0435\u043B\u044C\u0437\u044F.
1393
- - **\u041A\u043E\u043C\u043C\u0438\u0442\u044B \u0434\u0435\u043B\u0430\u0435\u0442 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C** \u2014 \u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \`git commit\`/\`git push\` \u0431\u0435\u0437 \u044F\u0432\u043D\u043E\u0439 \u043F\u0440\u043E\u0441\u044C\u0431\u044B.
1404
+ ## Boundaries (what the agent must not touch)
1405
+ - **Generated/managed files \u2014 do not edit by hand** (the CLI overwrites them from state):
1406
+ \`lib/slots.ts\`, \`lib/payments.ts\`, \`lib/blueprint.ts\`, the managed regions of \`site.config.ts\`
1407
+ (\`features\`/\`integrations\`), \`vitrine.json\`, the feature table in this \`CLAUDE.md\`, \`.env*\`.
1408
+ The set of features/integrations is changed via \`vitrine add/remove\`, not by editing files.
1409
+ - **Design \u2014 token values only** in \`theme/client.css\` (via \`vitrine design apply\`):
1410
+ don't change logic/data/routing/a11y/component structure.
1411
+ - **Contracts are extended additively** (\`@vitrine-kit/contracts\`): you must not break the shape of existing fields.
1412
+ - **The user makes commits** \u2014 don't run \`git commit\`/\`git push\` without an explicit request.
1394
1413
  `
1395
1414
  );
1396
- writeText(join11(root, "README.md"), clientReadme(name, backend, tier));
1397
- writeText(join11(root, "lib", "slots.ts"), renderSlotsFile([]));
1398
- writeText(join11(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1399
- writeText(join11(root, "theme", "client.css"), renderNeutralTheme());
1400
- writeText(join11(root, ".env.example"), clientEnvExample(backend));
1401
- writeText(join11(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1415
+ writeText(join12(root, "README.md"), clientReadme(name, backend, tier));
1416
+ writeText(join12(root, "lib", "slots.ts"), renderSlotsFile([]));
1417
+ writeText(join12(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1418
+ writeText(join12(root, "theme", "client.css"), renderNeutralTheme());
1419
+ writeText(join12(root, ".env.example"), clientEnvExample(backend));
1420
+ writeText(join12(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1402
1421
  `);
1403
1422
  }
1404
1423
  function initProject(opts) {
1405
1424
  if (exists(opts.root) && readdirSync3(opts.root).length > 0) {
1406
- throw new Error(`[vitrine] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 "${opts.root}" \u043D\u0435 \u043F\u0443\u0441\u0442\u043E\u0439`);
1425
+ throw new Error(`[vitrine] directory "${opts.root}" is not empty`);
1407
1426
  }
1408
1427
  scaffoldBase(opts);
1409
1428
  const project = loadProject(opts.root);
@@ -1412,9 +1431,9 @@ function initProject(opts) {
1412
1431
 
1413
1432
  // src/kit-update.ts
1414
1433
  import { spawnSync as spawnSync2 } from "child_process";
1415
- import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1434
+ import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1416
1435
  import { tmpdir } from "os";
1417
- import { join as join12, resolve as resolve5 } from "path";
1436
+ import { join as join13, resolve as resolve5 } from "path";
1418
1437
  var REPO = "vitrine-kit/vitrine";
1419
1438
  function hasBin(bin) {
1420
1439
  const probe = process.platform === "win32" ? "where" : "which";
@@ -1422,27 +1441,27 @@ function hasBin(bin) {
1422
1441
  }
1423
1442
  function acquireFromGh(version) {
1424
1443
  if (!hasBin("gh")) {
1425
- throw new Error("[vitrine] \u0434\u043B\u044F \u0441\u0435\u0442\u0435\u0432\u043E\u0433\u043E \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u043D\u0443\u0436\u0435\u043D gh (GitHub CLI), \u043B\u0438\u0431\u043E \u0443\u043A\u0430\u0436\u0438\u0442\u0435 --from <dir>");
1444
+ throw new Error("[vitrine] network update needs gh (GitHub CLI), or pass --from <dir>");
1426
1445
  }
1427
1446
  if (!hasBin("tar")) {
1428
- throw new Error("[vitrine] \u0434\u043B\u044F \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u043A\u0438 \u043D\u0443\u0436\u0435\u043D tar, \u043B\u0438\u0431\u043E \u0443\u043A\u0430\u0436\u0438\u0442\u0435 --from <dir>");
1447
+ throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
1429
1448
  }
1430
- const tmp = mkdtempSync(join12(tmpdir(), "vitrine-kit-"));
1449
+ const tmp = mkdtempSync(join13(tmpdir(), "vitrine-kit-"));
1431
1450
  const dl = spawnSync2(
1432
1451
  "gh",
1433
1452
  ["release", "download", ...version ? [version] : [], "--repo", REPO, "--archive=tar.gz", "--dir", tmp, "--clobber"],
1434
1453
  { stdio: "inherit" }
1435
1454
  );
1436
1455
  if (dl.status !== 0) {
1437
- throw new Error("[vitrine] gh release download \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F (\u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u0434\u043E\u0441\u0442\u0443\u043F/\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044E: gh auth status, \u043F\u0440\u0438 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0441\u0442\u0438 gh auth login)");
1456
+ throw new Error("[vitrine] gh release download failed (check access/auth: gh auth status, and gh auth login if needed)");
1438
1457
  }
1439
1458
  const tarball = readdirSync4(tmp).find((f) => f.endsWith(".tar.gz"));
1440
- if (!tarball) throw new Error("[vitrine] release-tarball \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043F\u043E\u0441\u043B\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438");
1441
- if (spawnSync2("tar", ["-xzf", join12(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1442
- throw new Error("[vitrine] \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u043A\u0430 tarball \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C");
1459
+ 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) {
1461
+ throw new Error("[vitrine] tarball extraction failed");
1443
1462
  }
1444
- const root = readdirSync4(tmp).map((f) => join12(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync8(join12(p2, "registry", "_index.json")));
1445
- if (!root) throw new Error("[vitrine] \u0432 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u043C tarball \u043D\u0435\u0442 registry/_index.json");
1463
+ const root = readdirSync4(tmp).map((f) => join13(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join13(p2, "registry", "_index.json")));
1464
+ if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
1446
1465
  return root;
1447
1466
  }
1448
1467
  function kitUpdate(opts = {}) {
@@ -1451,7 +1470,7 @@ function kitUpdate(opts = {}) {
1451
1470
  }
1452
1471
  function kitStatus(home = vitrineHome()) {
1453
1472
  const meta = readKitMeta(home);
1454
- const idxFile = join12(cachePaths(home).registry, "_index.json");
1473
+ const idxFile = join13(cachePaths(home).registry, "_index.json");
1455
1474
  const idx = exists(idxFile) ? readJson(idxFile) : null;
1456
1475
  return {
1457
1476
  cached: meta !== null,
@@ -1477,93 +1496,93 @@ function selfUpdate(opts = {}) {
1477
1496
  var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
1478
1497
  var program = new Command();
1479
1498
  program.name("vitrine").description("Vitrine CLI").version(pkg.version);
1480
- program.command("add").description("\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0444\u0438\u0447\u0443(\u0438) \u0432 \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439").argument("<features...>", "\u0438\u043C\u0435\u043D\u0430 \u0444\u0438\u0447").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").action((features, opts) => {
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) => {
1481
1500
  const res = addFeatures(features, opts.registry);
1482
- console.log(res.installed.length ? `\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${res.installed.join(", ")}` : "\u0423\u0436\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u2014 \u043D\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439.");
1501
+ console.log(res.installed.length ? `Installed: ${res.installed.join(", ")}` : "Already installed \u2014 no changes.");
1483
1502
  });
1484
- program.command("remove").description("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0438\u0447\u0443 (\u0435\u0441\u043B\u0438 removable)").argument("<feature>", "\u0438\u043C\u044F \u0444\u0438\u0447\u0438").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").action((feature, opts) => {
1503
+ program.command("remove").description("Remove a feature (if removable)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").action((feature, opts) => {
1485
1504
  removeFeatureCmd(feature, opts.registry);
1486
- console.log(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${feature}`);
1505
+ console.log(`Removed: ${feature}`);
1487
1506
  });
1488
- program.command("update").description("\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0443\u044E \u0444\u0438\u0447\u0443 (3-way merge); \u0431\u0435\u0437 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u2014 \u0432\u0441\u0435").argument("[features...]", "\u0438\u043C\u0435\u043D\u0430 \u0444\u0438\u0447").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").option("--dry-run", "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u043B\u0430\u043D \u0431\u0435\u0437 \u0437\u0430\u043F\u0438\u0441\u0438").action((features, opts) => {
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) => {
1489
1508
  const outcomes = updateFeaturesCmd(features, opts.registry, { dryRun: opts.dryRun });
1490
1509
  let conflicts = 0;
1491
1510
  for (const { plan, applied } of outcomes) {
1492
1511
  console.log(renderPlan(plan));
1493
- if (applied) console.log(" \u2192 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E");
1512
+ if (applied) console.log(" \u2192 applied");
1494
1513
  conflicts += plan.files.reduce((n, f) => n + f.conflicts, 0);
1495
1514
  }
1496
1515
  if (conflicts > 0) {
1497
1516
  console.log(`
1498
- \u26A0 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432: ${conflicts}. \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 git-\u043C\u0430\u0440\u043A\u0435\u0440\u044B (<<<<<<< / >>>>>>>) \u0432\u0440\u0443\u0447\u043D\u0443\u044E.`);
1517
+ \u26A0 conflicts: ${conflicts}. Resolve the git markers (<<<<<<< / >>>>>>>) by hand.`);
1499
1518
  }
1500
1519
  });
1501
- program.command("diff").description("\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 update (\u0431\u0435\u0437 \u0437\u0430\u043F\u0438\u0441\u0438)").argument("<feature>", "\u0438\u043C\u044F \u0444\u0438\u0447\u0438").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").action((feature, opts) => {
1520
+ program.command("diff").description("Preview update changes (without writing)").argument("<feature>", "feature name").option("--registry <path>", "path to the registry").action((feature, opts) => {
1502
1521
  console.log(renderPlan(diffFeatureCmd(feature, opts.registry)));
1503
1522
  });
1504
- program.command("list").description("\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u0444\u0438\u0447\u0438").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").action((opts) => {
1523
+ program.command("list").description("Installed and available features").option("--registry <path>", "path to the registry").action((opts) => {
1505
1524
  const { installed, available } = listFeatures(opts.registry);
1506
- console.log("\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B:", installed.join(", ") || "\u2014");
1507
- console.log("\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B: ", available.join(", ") || "\u2014");
1525
+ console.log("Installed:", installed.join(", ") || "\u2014");
1526
+ console.log("Available:", available.join(", ") || "\u2014");
1508
1527
  });
1509
- var design = program.command("design").description("\u0418\u0418-\u0448\u0430\u0433 \u0434\u0438\u0437\u0430\u0439\u043D\u0430 (\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u043D\u0430\u0434 Claude Code)");
1510
- design.command("apply").description("\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0438\u0437\u0430\u0439\u043D \u0438\u0437 /design \u043A \u0442\u043E\u043A\u0435\u043D\u0430\u043C (\u0447\u0435\u0440\u0435\u0437 Claude Code)").option("--bin <path>", "\u043F\u0443\u0442\u044C \u043A Claude Code (\u0438\u043D\u0430\u0447\u0435 VITRINE_CLAUDE_BIN / PATH)").option("--dry-run", "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u0431\u0435\u0437 \u0437\u0430\u043F\u0443\u0441\u043A\u0430").action((opts) => {
1528
+ 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) => {
1511
1530
  const code = designApplyCmd({ bin: opts.bin, dryRun: opts.dryRun });
1512
1531
  if (code !== 0) process.exit(code);
1513
1532
  });
1514
- var kit = program.command("kit").description("\u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 (\u043A\u044D\u0448 \u0440\u0435\u0435\u0441\u0442\u0440\u0430 ~/.vitrine)");
1515
- kit.command("update").description("\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u044D\u0448 \u0440\u0435\u0435\u0441\u0442\u0440\u0430/\u0448\u0430\u0431\u043B\u043E\u043D\u043E\u0432 \u0441 GitHub (\u0438\u043B\u0438 --from <dir>)").option("--from <path>", "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0435 \u0434\u0435\u0440\u0435\u0432\u043E kit (\u043A\u043B\u043E\u043D / \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0439 tarball) \u0432\u043C\u0435\u0441\u0442\u043E \u0441\u0435\u0442\u0438").option("--version <tag>", "\u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0439 \u0440\u0435\u043B\u0438\u0437").option("--channel <channel>", "stable | main", "stable").action((opts) => {
1533
+ var kit = program.command("kit").description("Local tooling (the ~/.vitrine registry cache)");
1534
+ kit.command("update").description("Update the local registry/templates cache from GitHub (or --from <dir>)").option("--from <path>", "local kit tree (clone / unpacked tarball) instead of the network").option("--version <tag>", "a specific release").option("--channel <channel>", "stable | main", "stable").action((opts) => {
1516
1535
  const res = kitUpdate({ from: opts.from, version: opts.version, channel: opts.channel });
1517
- console.log(`\u041A\u044D\u0448 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0434\u043E kit ${res.kitVersion}.`);
1536
+ console.log(`Cache updated to kit ${res.kitVersion}.`);
1518
1537
  console.log(formatChangelog(res.changelog));
1519
1538
  });
1520
- kit.command("status").description("\u0412\u0435\u0440\u0441\u0438\u044F \u043A\u044D\u0448\u0430 \u0438 \u0447\u0442\u043E \u043D\u043E\u0432\u0435\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E CLI").action(() => {
1539
+ kit.command("status").description("Cache version and what is newer than the installed CLI").action(() => {
1521
1540
  const s = kitStatus();
1522
1541
  if (!s.cached) {
1523
- console.log('\u041A\u044D\u0448 \u043F\u0443\u0441\u0442. \u0417\u0430\u043F\u0443\u0441\u0442\u0438 "vitrine kit update".');
1542
+ console.log('Cache is empty. Run "vitrine kit update".');
1524
1543
  return;
1525
1544
  }
1526
- console.log(`\u041A\u044D\u0448: kit ${s.kitVersion} (${s.channel}), \u0444\u0438\u0447: ${s.featureCount ?? "\u2014"}, \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D ${s.updatedAt}.`);
1527
- console.log(`CLI \u043E\u0436\u0438\u0434\u0430\u0435\u0442 kit ${s.cliKitVersion}.`);
1545
+ console.log(`Cache: kit ${s.kitVersion} (${s.channel}), features: ${s.featureCount ?? "\u2014"}, updated ${s.updatedAt}.`);
1546
+ console.log(`CLI expects kit ${s.cliKitVersion}.`);
1528
1547
  });
1529
- program.command("self-update").description("\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0430\u043C CLI (@vitrine-kit/vitrine)").option("--dry-run", "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u0431\u0435\u0437 \u0437\u0430\u043F\u0443\u0441\u043A\u0430").action((opts) => {
1548
+ program.command("self-update").description("Update the CLI itself (@vitrine-kit/vitrine)").option("--dry-run", "show the command without running").action((opts) => {
1530
1549
  const code = selfUpdate({ dryRun: opts.dryRun });
1531
1550
  if (code !== 0) process.exit(code);
1532
1551
  });
1533
- program.command("doctor").description("\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043A\u043E\u043D\u0441\u0438\u0441\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C: vitrine.json \u2194 \u0444\u0430\u0439\u043B\u044B \u2194 \u043F\u0430\u043A\u0435\u0442\u044B \u2194 env").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").action((opts) => {
1552
+ program.command("doctor").description("Check consistency: vitrine.json \u2194 files \u2194 packages \u2194 env").option("--registry <path>", "path to the registry").action((opts) => {
1534
1553
  const report = doctorCmd(opts.registry);
1535
1554
  if (report.issues.length === 0) {
1536
- console.log("\u2713 \u041F\u0440\u043E\u0431\u043B\u0435\u043C \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.");
1555
+ console.log("\u2713 No issues found.");
1537
1556
  return;
1538
1557
  }
1539
1558
  for (const i of report.issues) {
1540
- const tag = i.severity === "error" ? "\u041E\u0428\u0418\u0411\u041A\u0410" : "\u043F\u0440\u0435\u0434\u0443\u043F\u0440.";
1559
+ const tag = i.severity === "error" ? "ERROR" : "warn";
1541
1560
  console.log(`[${tag}] ${i.scope}: ${i.message}${i.fix ? ` \u2192 ${i.fix}` : ""}`);
1542
1561
  }
1543
1562
  if (!report.ok) process.exit(1);
1544
1563
  });
1545
- program.command("init").description("\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u043E\u0432\u044B\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442\u0430").argument("[name]", "\u0438\u043C\u044F \u043F\u0440\u043E\u0435\u043A\u0442\u0430").option("--dir <path>", "\u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433", process.cwd()).option("--tier <tier>", "catalog | simple-store | full-store").option("--backend <backend>", "payload | vendure").option("--features <list>", "\u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u044F\u0442\u0443\u044E").option("--registry <path>", "\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u0435\u0441\u0442\u0440\u0443").option("--yes", "\u0431\u0435\u0437 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432").action(async (nameArg, opts) => {
1564
+ program.command("init").description("Create a new client repository").argument("[name]", "project name").option("--dir <path>", "parent directory", process.cwd()).option("--tier <tier>", "catalog | simple-store | full-store").option("--backend <backend>", "payload | vendure").option("--features <list>", "comma-separated list").option("--registry <path>", "path to the registry").option("--yes", "skip interactive questions").action(async (nameArg, opts) => {
1546
1565
  const registry = createRegistrySource(opts.registry);
1547
1566
  let name = nameArg;
1548
1567
  let tier = opts.tier;
1549
1568
  let features = opts.features ? String(opts.features).split(",") : void 0;
1550
1569
  if (!opts.yes && (!name || !tier)) {
1551
1570
  p.intro("vitrine init");
1552
- if (!name) name = String(await p.text({ message: "\u0418\u043C\u044F \u043F\u0440\u043E\u0435\u043A\u0442\u0430", placeholder: "my-shop" }));
1571
+ if (!name) name = String(await p.text({ message: "Project name", placeholder: "my-shop" }));
1553
1572
  if (!tier) {
1554
1573
  tier = await p.select({
1555
- message: "\u0423\u0440\u043E\u0432\u0435\u043D\u044C",
1574
+ message: "Tier",
1556
1575
  options: [
1557
- { value: "catalog", label: "\u041A\u0430\u0442\u0430\u043B\u043E\u0433" },
1558
- { value: "simple-store", label: "\u041F\u0440\u043E\u0441\u0442\u043E\u0439 \u043C\u0430\u0433\u0430\u0437\u0438\u043D" },
1559
- { value: "full-store", label: "\u041F\u043E\u043B\u043D\u044B\u0439 \u043C\u0430\u0433\u0430\u0437\u0438\u043D" }
1576
+ { value: "catalog", label: "Catalog" },
1577
+ { value: "simple-store", label: "Simple store" },
1578
+ { value: "full-store", label: "Full store" }
1560
1579
  ]
1561
1580
  });
1562
1581
  }
1563
1582
  const suggested = suggestFeatures(tier, registry);
1564
1583
  const baseline = suggested.filter((f) => !PAYMENT_PROVIDER_FEATURES.includes(f));
1565
1584
  const picked = await p.multiselect({
1566
- message: "\u0424\u0438\u0447\u0438",
1585
+ message: "Features",
1567
1586
  options: baseline.map((f) => ({ value: f, label: f })),
1568
1587
  initialValues: baseline,
1569
1588
  required: false
@@ -1574,14 +1593,14 @@ program.command("init").description("\u0421\u043E\u0437\u0434\u0430\u0442\u044C
1574
1593
  const labels = {
1575
1594
  "checkout-stripe": "Stripe",
1576
1595
  "checkout-paddle": "Paddle",
1577
- "checkout-yookassa": "\u042EKassa"
1596
+ "checkout-yookassa": "YooKassa"
1578
1597
  };
1579
1598
  const available = PAYMENT_PROVIDER_FEATURES.filter((f) => registry.hasFeature(f));
1580
1599
  if (available.length > 0) {
1581
1600
  const provider = await p.select({
1582
- message: "\u041F\u043B\u0430\u0442\u0451\u0436\u043D\u044B\u0439 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440",
1601
+ message: "Payment provider",
1583
1602
  options: [
1584
- { value: "none", label: "\u041D\u0435\u0442 (\u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u043E\u0437\u0436\u0435: vitrine add checkout-<provider>)" },
1603
+ { value: "none", label: "None (add later: vitrine add checkout-<provider>)" },
1585
1604
  ...available.map((f) => ({ value: f, label: labels[f] ?? f }))
1586
1605
  ],
1587
1606
  initialValue: available[0]
@@ -1589,16 +1608,16 @@ program.command("init").description("\u0421\u043E\u0437\u0434\u0430\u0442\u044C
1589
1608
  if (typeof provider === "string" && provider !== "none") features.push(provider);
1590
1609
  }
1591
1610
  }
1592
- p.outro("\u041F\u043E\u0435\u0445\u0430\u043B\u0438");
1611
+ p.outro("Ready");
1593
1612
  }
1594
- if (!name) throw new Error("[vitrine] \u043D\u0443\u0436\u043D\u043E \u0438\u043C\u044F \u043F\u0440\u043E\u0435\u043A\u0442\u0430");
1613
+ if (!name) throw new Error("[vitrine] a project name is required");
1595
1614
  const finalTier = tier ?? "catalog";
1596
1615
  const backend = opts.backend ?? defaultBackend(finalTier);
1597
1616
  const finalFeatures = features ?? suggestFeatures(finalTier, registry, backend);
1598
1617
  const root = resolve6(String(opts.dir ?? process.cwd()), name);
1599
1618
  const res = initProject({ root, name, backend, tier: finalTier, features: finalFeatures, registry });
1600
- console.log(`\u0421\u043E\u0437\u0434\u0430\u043D \u043F\u0440\u043E\u0435\u043A\u0442 "${name}" \u2192 ${root}`);
1601
- console.log(`\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${res.installed.join(", ") || "\u2014"}`);
1619
+ console.log(`Created project "${name}" \u2192 ${root}`);
1620
+ console.log(`Installed: ${res.installed.join(", ") || "\u2014"}`);
1602
1621
  });
1603
1622
  async function main() {
1604
1623
  preflightNode();