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