@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.
- package/README.md +18 -18
- package/dist/index.js +304 -285
- package/kit/registry/README.md +9 -0
- package/kit/registry/_index.json +74 -0
- package/kit/registry/cart/docs/cart.md +13 -0
- package/kit/registry/cart/feature.json +51 -0
- package/kit/registry/cart/files/app/(frontend)/cart/page.tsx +21 -0
- package/kit/registry/cart/files/app/api/cart/route.ts +41 -0
- package/kit/registry/cart/files/components/cart/AddToCart.tsx +40 -0
- package/kit/registry/cart/files/components/cart/CartIndicator.tsx +11 -0
- package/kit/registry/cart/files/components/cart/CartLineItem.tsx +27 -0
- package/kit/registry/cart/files/components/cart/CartSummary.tsx +31 -0
- package/kit/registry/cart/files/components/cart/CartView.tsx +31 -0
- package/kit/registry/cart/files/lib/cart/data.ts +12 -0
- package/kit/registry/cart/files/lib/cart/register.ts +10 -0
- package/kit/registry/catalog/docs/catalog.md +18 -0
- package/kit/registry/catalog/feature.json +43 -0
- package/kit/registry/catalog/files/components/catalog/CategoryNav.tsx +26 -0
- package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +42 -0
- package/kit/registry/catalog/files/components/catalog/ProductGrid.tsx +25 -0
- package/kit/registry/catalog/files/lib/catalog/data.ts +24 -0
- package/kit/registry/catalog/files/lib/catalog/register.ts +8 -0
- package/kit/registry/checkout/docs/checkout.md +17 -0
- package/kit/registry/checkout/feature.json +47 -0
- package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -0
- package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +31 -0
- package/kit/registry/checkout/files/lib/checkout/fulfill.ts +72 -0
- package/kit/registry/checkout/files/lib/checkout/register.ts +8 -0
- package/kit/registry/checkout-paddle/docs/checkout-paddle.md +22 -0
- package/kit/registry/checkout-paddle/feature.json +62 -0
- package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +22 -0
- package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +59 -0
- package/kit/registry/checkout-paddle/files/lib/checkout-paddle/register.ts +7 -0
- package/kit/registry/checkout-stripe/docs/checkout-stripe.md +17 -0
- package/kit/registry/checkout-stripe/feature.json +54 -0
- package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +23 -0
- package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +73 -0
- package/kit/registry/checkout-stripe/files/lib/checkout-stripe/register.ts +7 -0
- package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +20 -0
- package/kit/registry/checkout-yookassa/feature.json +52 -0
- package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +22 -0
- package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +82 -0
- package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/register.ts +7 -0
- package/kit/registry/product-page/docs/product-page.md +16 -0
- package/kit/registry/product-page/feature.json +39 -0
- package/kit/registry/product-page/files/components/product/ProductGallery.tsx +37 -0
- package/kit/registry/product-page/files/components/product/ProductView.tsx +42 -0
- package/kit/registry/product-page/files/lib/product/data.ts +10 -0
- package/kit/registry/seo/docs/seo.md +14 -0
- package/kit/registry/seo/feature.json +35 -0
- package/kit/registry/seo/files/components/seo/JsonLd.tsx +15 -0
- package/kit/registry/seo/files/lib/seo/jsonld.ts +29 -0
- package/kit/registry/seo/files/lib/seo/metadata.ts +35 -0
- package/kit/registry/stubs/route-glue.d.ts +45 -0
- package/kit/registry/tsconfig.json +21 -0
- package/kit/registry/tsconfig.routes.json +9 -0
- package/kit/templates/README.md +27 -0
- package/kit/templates/backend-payload/files/.dockerignore +8 -0
- package/kit/templates/backend-payload/files/Dockerfile +29 -0
- package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/not-found.tsx +18 -0
- package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/page.tsx +18 -0
- package/kit/templates/backend-payload/files/app/(payload)/admin/importMap.js +3 -0
- package/kit/templates/backend-payload/files/app/(payload)/api/[...slug]/route.ts +18 -0
- package/kit/templates/backend-payload/files/app/(payload)/api/graphql/route.ts +6 -0
- package/kit/templates/backend-payload/files/app/(payload)/api/graphql-playground/route.ts +5 -0
- package/kit/templates/backend-payload/files/app/(payload)/custom.scss +1 -0
- package/kit/templates/backend-payload/files/app/(payload)/layout.tsx +23 -0
- package/kit/templates/backend-payload/files/docker-compose.yml +35 -0
- package/kit/templates/backend-payload/files/lib/adapter/db-decision.ts +43 -0
- package/kit/templates/backend-payload/files/lib/adapter/db-ping.ts +29 -0
- package/kit/templates/backend-payload/files/lib/adapter/db.ts +27 -0
- package/kit/templates/backend-payload/files/lib/adapter/index.ts +31 -0
- package/kit/templates/backend-payload/files/lib/adapter/map.ts +112 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +79 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +154 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-types.ts +50 -0
- package/kit/templates/backend-payload/files/lib/seed/admin.ts +23 -0
- package/kit/templates/backend-payload/files/lib/seed/demo.ts +79 -0
- package/kit/templates/backend-payload/files/lib/seed/guards.ts +5 -0
- package/kit/templates/backend-payload/files/lib/seed/richtext.ts +26 -0
- package/kit/templates/backend-payload/files/lib/seed/run.ts +51 -0
- package/kit/templates/backend-payload/files/next.config.mjs +10 -0
- package/kit/templates/backend-payload/files/payload.config.ts +32 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-1.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-2.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-3.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-4.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-5.svg +5 -0
- package/kit/templates/backend-vendure/files/.dockerignore +8 -0
- package/kit/templates/backend-vendure/files/Dockerfile +24 -0
- package/kit/templates/backend-vendure/files/docker-compose.yml +50 -0
- package/kit/templates/backend-vendure/files/lib/adapter/graphql.ts +27 -0
- package/kit/templates/backend-vendure/files/lib/adapter/index.ts +20 -0
- package/kit/templates/backend-vendure/files/lib/adapter/map.ts +107 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +62 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-commerce.ts +76 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-types.ts +61 -0
- package/kit/templates/backend-vendure/files/lib/seed/populate.ts +22 -0
- package/kit/templates/backend-vendure/files/src/index.ts +15 -0
- package/kit/templates/backend-vendure/files/vendure-config.ts +56 -0
- package/kit/templates/base/files/.claude/commands/add-feature.md +18 -0
- package/kit/templates/base/files/.claude/commands/design.md +14 -0
- package/kit/templates/base/files/.claude/commands/doctor.md +15 -0
- package/kit/templates/base/files/.claude/commands/setup.md +15 -0
- package/kit/templates/base/files/.claude/commands/update.md +16 -0
- package/kit/templates/base/files/AGENTS.md +20 -0
- package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +28 -0
- package/kit/templates/base/files/app/(frontend)/layout.tsx +32 -0
- package/kit/templates/base/files/app/(frontend)/page.tsx +27 -0
- package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +43 -0
- package/kit/templates/base/files/app/globals.css +6 -0
- package/kit/templates/base/files/components/layout/SiteFooter.tsx +15 -0
- package/kit/templates/base/files/components/layout/SiteHeader.tsx +23 -0
- package/kit/templates/base/files/design/README.md +9 -0
- package/kit/templates/base/files/lib/site.ts +10 -0
- package/kit/templates/base/files/next.config.mjs +7 -0
- package/kit/templates/base/files/postcss.config.mjs +7 -0
- package/kit/templates/base/files/public/.gitkeep +0 -0
- package/kit/templates/base/files/tailwind.config.ts +13 -0
- package/kit/templates/base/files/tsconfig.json +25 -0
- package/kit/templates/tsconfig.json +18 -0
- 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]
|
|
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]
|
|
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]
|
|
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
|
|
125
|
-
import { dirname as
|
|
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
|
|
130
|
-
import { join as
|
|
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]
|
|
135
|
-
return
|
|
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:
|
|
141
|
-
templates:
|
|
142
|
-
meta:
|
|
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 =
|
|
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 "
|
|
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 =
|
|
184
|
-
const srcTemplates =
|
|
185
|
-
if (!
|
|
186
|
-
throw new Error(`[vitrine]
|
|
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 (
|
|
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 =
|
|
211
|
-
if (
|
|
212
|
-
const parent =
|
|
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 =
|
|
228
|
-
if (
|
|
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
|
-
|
|
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) =>
|
|
238
|
-
const hasFeature = (name) =>
|
|
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 =
|
|
243
|
-
if (!
|
|
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(
|
|
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
|
|
269
|
+
import { join as join7 } from "path";
|
|
257
270
|
|
|
258
271
|
// src/transaction.ts
|
|
259
|
-
import { existsSync as
|
|
260
|
-
import { dirname as
|
|
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 (
|
|
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(
|
|
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 (
|
|
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 (
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 "
|
|
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 ["|
|
|
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:
|
|
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
|
|
468
|
+
import { join as join6 } from "path";
|
|
452
469
|
function* eachFeatureFile(featDir, map) {
|
|
453
|
-
const src =
|
|
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 ?
|
|
458
|
-
const repoRel = rel ?
|
|
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]
|
|
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]
|
|
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]
|
|
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 =
|
|
511
|
+
const originalsBase = join7(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
|
|
495
512
|
for (const map of manifest.files) {
|
|
496
|
-
if (!exists(
|
|
497
|
-
throw new Error(`[vitrine]
|
|
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]
|
|
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]
|
|
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]
|
|
610
|
+
throw new Error(`[vitrine] cannot remove "${name}": "${other}" depends on it`);
|
|
594
611
|
}
|
|
595
612
|
}
|
|
596
|
-
const originalsDir =
|
|
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
|
|
620
|
-
import { delimiter, join as
|
|
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 = "##
|
|
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 (
|
|
627
|
-
throw new Error(`[vitrine] Claude Code
|
|
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 =
|
|
633
|
-
if (
|
|
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
|
|
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 =
|
|
642
|
-
if (!
|
|
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 =
|
|
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
|
-
"
|
|
659
|
-
"-
|
|
660
|
-
"-
|
|
661
|
-
"-
|
|
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
|
-
"
|
|
665
|
-
"
|
|
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]
|
|
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
|
|
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
|
|
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:
|
|
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("
|
|
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: "
|
|
715
|
-
fix: "
|
|
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:
|
|
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:
|
|
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(
|
|
753
|
+
if (!exists(join9(project.root, file.repoRel))) {
|
|
737
754
|
add({
|
|
738
755
|
severity: "error",
|
|
739
756
|
scope,
|
|
740
|
-
message:
|
|
741
|
-
fix: `vitrine add ${name} (
|
|
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:
|
|
752
|
-
fix: "
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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 (
|
|
855
|
-
const theirLabel = labels.theirs ?? "theirs (
|
|
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]
|
|
925
|
-
if (!registry.hasFeature(name)) throw new Error(`[vitrine]
|
|
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 =
|
|
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 =
|
|
936
|
-
const basePath =
|
|
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 =
|
|
965
|
-
const oldOriginals =
|
|
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
|
|
991
|
-
return ` ${mark} ${f.to}${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}:
|
|
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]
|
|
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
|
|
1057
|
+
import { join as join12 } from "path";
|
|
1041
1058
|
|
|
1042
|
-
// src/kit.ts
|
|
1043
|
-
var KIT_VERSION = "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.
|
|
1047
|
-
var BLUEPRINT_RANGE = "^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
|
|
1055
|
-
import { dirname as
|
|
1073
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1074
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
1056
1075
|
function templatesRoot(registryRoot) {
|
|
1057
|
-
return
|
|
1076
|
+
return join11(dirname6(registryRoot), "templates");
|
|
1058
1077
|
}
|
|
1059
1078
|
function hasTemplate(root, name) {
|
|
1060
|
-
return
|
|
1079
|
+
return existsSync8(join11(root, name, "files"));
|
|
1061
1080
|
}
|
|
1062
1081
|
function copyTemplate(root, name, destRoot) {
|
|
1063
|
-
const filesDir =
|
|
1082
|
+
const filesDir = join11(root, name, "files");
|
|
1064
1083
|
const rels = walkRelFiles(filesDir);
|
|
1065
|
-
for (const rel of rels) writeText(
|
|
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"
|
|
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
|
-
"#
|
|
1182
|
+
"# Project environment.",
|
|
1164
1183
|
"",
|
|
1165
|
-
"#
|
|
1184
|
+
"# DB. Empty in dev \u2014 built-in SQLite (.vitrine/vendure.sqlite).",
|
|
1166
1185
|
"DATABASE_URL=",
|
|
1167
1186
|
"",
|
|
1168
|
-
"# Vendure Shop API (
|
|
1187
|
+
"# Vendure Shop API (storefront \u2192 server).",
|
|
1169
1188
|
"VENDURE_SHOP_API_URL=http://localhost:3001/shop-api",
|
|
1170
1189
|
"",
|
|
1171
|
-
"#
|
|
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
|
-
"#
|
|
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 "#
|
|
1201
|
+
return "# Project environment.\nDATABASE_URL=\n";
|
|
1183
1202
|
}
|
|
1184
1203
|
return [
|
|
1185
|
-
"#
|
|
1204
|
+
"# Project environment.",
|
|
1186
1205
|
"",
|
|
1187
|
-
"#
|
|
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
|
-
"#
|
|
1209
|
+
"# Payload secret (required; generate a random one for prod).",
|
|
1191
1210
|
"PAYLOAD_SECRET=",
|
|
1192
1211
|
"",
|
|
1193
|
-
"# Dev
|
|
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
|
-
"#
|
|
1216
|
+
"# Site base URL (canonical, OG).",
|
|
1198
1217
|
"NEXT_PUBLIC_SITE_URL=http://localhost:3000",
|
|
1199
1218
|
"",
|
|
1200
|
-
"#
|
|
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
|
|
1211
|
-
"pnpm dev #
|
|
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
|
-
"
|
|
1215
|
-
"populate
|
|
1216
|
-
"superadmin/superadmin;
|
|
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
|
-
"-
|
|
1225
|
-
"-
|
|
1243
|
+
"- Storefront: http://localhost:3000",
|
|
1244
|
+
"- Admin: http://localhost:3000/admin",
|
|
1226
1245
|
"",
|
|
1227
|
-
"
|
|
1228
|
-
"
|
|
1229
|
-
"
|
|
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=... #
|
|
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
|
-
|
|
1235
|
-
Next.js + Tailwind;
|
|
1236
|
-
|
|
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.
|
|
1257
|
+
## 1. Prerequisites
|
|
1239
1258
|
|
|
1240
|
-
Node >= 20 (LTS)
|
|
1241
|
-
|
|
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.
|
|
1262
|
+
## 2. Local run (zero-config)
|
|
1244
1263
|
|
|
1245
1264
|
${run}
|
|
1246
1265
|
|
|
1247
|
-
## 3.
|
|
1266
|
+
## 3. Apply the client design
|
|
1248
1267
|
|
|
1249
|
-
1.
|
|
1250
|
-
2. \`vitrine design apply\` \u2014
|
|
1251
|
-
(
|
|
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.
|
|
1272
|
+
## 4. Features: add, remove, view
|
|
1254
1273
|
|
|
1255
1274
|
\`\`\`bash
|
|
1256
|
-
vitrine list #
|
|
1257
|
-
vitrine add
|
|
1258
|
-
vitrine remove
|
|
1259
|
-
vitrine design apply #
|
|
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\`
|
|
1263
|
-
\`.vitrine/originals/\` \u2014
|
|
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.
|
|
1284
|
+
## 5. Updates and checks
|
|
1266
1285
|
|
|
1267
1286
|
\`\`\`bash
|
|
1268
|
-
vitrine kit update #
|
|
1269
|
-
vitrine diff <feature> #
|
|
1270
|
-
vitrine update [feature] # 3-way merge
|
|
1271
|
-
vitrine doctor #
|
|
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
|
-
|
|
1275
|
-
\`@vitrine-kit/core\`,
|
|
1276
|
-
|
|
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.
|
|
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
|
|
1286
|
-
(SQLite
|
|
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(
|
|
1316
|
+
writeText(join12(root, ".gitignore"), "node_modules/\n.next/\ndist/\n.env\n.env.local\n.vitrine/\n");
|
|
1298
1317
|
}
|
|
1299
1318
|
writeText(
|
|
1300
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
1349
|
+
join12(root, "CLAUDE.md"),
|
|
1331
1350
|
`# ${name}
|
|
1332
1351
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
##
|
|
1356
|
+
## Installed features
|
|
1338
1357
|
<!-- vitrine:features:start -->
|
|
1339
|
-
|
|
1358
|
+
_No features installed yet._
|
|
1340
1359
|
<!-- vitrine:features:end -->
|
|
1341
1360
|
|
|
1342
|
-
##
|
|
1361
|
+
## vitrine CLI commands
|
|
1343
1362
|
|
|
1344
|
-
|
|
|
1363
|
+
| Command | Purpose | When to use | Flags |
|
|
1345
1364
|
|---|---|---|---|
|
|
1346
|
-
| \`vitrine list\` |
|
|
1347
|
-
| \`vitrine add <features\u2026>\` |
|
|
1348
|
-
| \`vitrine remove <feature>\` |
|
|
1349
|
-
| \`vitrine update [features\u2026]\` |
|
|
1350
|
-
| \`vitrine diff <feature>\` |
|
|
1351
|
-
| \`vitrine doctor\` |
|
|
1352
|
-
| \`vitrine design apply\` |
|
|
1353
|
-
| \`vitrine kit update\` |
|
|
1354
|
-
| \`vitrine kit status\` |
|
|
1355
|
-
| \`vitrine self-update\` |
|
|
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\`
|
|
1358
|
-
|
|
1359
|
-
\u2014
|
|
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
|
-
##
|
|
1380
|
+
## Common scenarios
|
|
1362
1381
|
|
|
1363
|
-
-
|
|
1364
|
-
-
|
|
1365
|
-
-
|
|
1366
|
-
-
|
|
1367
|
-
-
|
|
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
|
-
|
|
1388
|
+
The full human guide is in \`README.md\`.
|
|
1370
1389
|
|
|
1371
|
-
##
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
1)
|
|
1377
|
-
2)
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
a11y
|
|
1381
|
-
|
|
1382
|
-
(
|
|
1383
|
-
|
|
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
|
-
##
|
|
1386
|
-
-
|
|
1387
|
-
\`lib/slots.ts\`, \`lib/payments.ts\`, \`lib/blueprint.ts\`,
|
|
1388
|
-
(\`features\`/\`integrations\`), \`vitrine.json\`,
|
|
1389
|
-
|
|
1390
|
-
-
|
|
1391
|
-
|
|
1392
|
-
-
|
|
1393
|
-
-
|
|
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(
|
|
1397
|
-
writeText(
|
|
1398
|
-
writeText(
|
|
1399
|
-
writeText(
|
|
1400
|
-
writeText(
|
|
1401
|
-
writeText(
|
|
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]
|
|
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
|
|
1434
|
+
import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
1416
1435
|
import { tmpdir } from "os";
|
|
1417
|
-
import { join as
|
|
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]
|
|
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]
|
|
1447
|
+
throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
|
|
1429
1448
|
}
|
|
1430
|
-
const tmp = mkdtempSync(
|
|
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
|
|
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
|
|
1441
|
-
if (spawnSync2("tar", ["-xzf",
|
|
1442
|
-
throw new Error("[vitrine]
|
|
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) =>
|
|
1445
|
-
if (!root) throw new Error("[vitrine]
|
|
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 =
|
|
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("
|
|
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 ?
|
|
1501
|
+
console.log(res.installed.length ? `Installed: ${res.installed.join(", ")}` : "Already installed \u2014 no changes.");
|
|
1483
1502
|
});
|
|
1484
|
-
program.command("remove").description("
|
|
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(
|
|
1505
|
+
console.log(`Removed: ${feature}`);
|
|
1487
1506
|
});
|
|
1488
|
-
program.command("update").description("
|
|
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
|
|
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
|
|
1517
|
+
\u26A0 conflicts: ${conflicts}. Resolve the git markers (<<<<<<< / >>>>>>>) by hand.`);
|
|
1499
1518
|
}
|
|
1500
1519
|
});
|
|
1501
|
-
program.command("diff").description("
|
|
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("
|
|
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("
|
|
1507
|
-
console.log("
|
|
1525
|
+
console.log("Installed:", installed.join(", ") || "\u2014");
|
|
1526
|
+
console.log("Available:", available.join(", ") || "\u2014");
|
|
1508
1527
|
});
|
|
1509
|
-
var design = program.command("design").description("
|
|
1510
|
-
design.command("apply").description("
|
|
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("
|
|
1515
|
-
kit.command("update").description("
|
|
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(
|
|
1536
|
+
console.log(`Cache updated to kit ${res.kitVersion}.`);
|
|
1518
1537
|
console.log(formatChangelog(res.changelog));
|
|
1519
1538
|
});
|
|
1520
|
-
kit.command("status").description("
|
|
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('
|
|
1542
|
+
console.log('Cache is empty. Run "vitrine kit update".');
|
|
1524
1543
|
return;
|
|
1525
1544
|
}
|
|
1526
|
-
console.log(
|
|
1527
|
-
console.log(`CLI
|
|
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("
|
|
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("
|
|
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
|
|
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" ? "
|
|
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("
|
|
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: "
|
|
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: "
|
|
1574
|
+
message: "Tier",
|
|
1556
1575
|
options: [
|
|
1557
|
-
{ value: "catalog", label: "
|
|
1558
|
-
{ value: "simple-store", label: "
|
|
1559
|
-
{ value: "full-store", label: "
|
|
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: "
|
|
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": "
|
|
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: "
|
|
1601
|
+
message: "Payment provider",
|
|
1583
1602
|
options: [
|
|
1584
|
-
{ value: "none", label: "
|
|
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("
|
|
1611
|
+
p.outro("Ready");
|
|
1593
1612
|
}
|
|
1594
|
-
if (!name) throw new Error("[vitrine]
|
|
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(
|
|
1601
|
-
console.log(
|
|
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();
|