@vitrine-kit/vitrine 0.3.0 → 0.3.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/README.md +18 -18
- package/dist/index.js +205 -205
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
# @vitrine-kit/vitrine (CLI)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The Vitrine toolkit — an agentic starter kit for e-commerce stores and catalogs.
|
|
4
|
+
Install it globally:
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
npm i -g @vitrine-kit/vitrine
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## From zero to a running project
|
|
11
11
|
|
|
12
|
-
1.
|
|
12
|
+
1. **Create the client repository** — wizard (or flags for CI):
|
|
13
13
|
```bash
|
|
14
14
|
vitrine init my-shop
|
|
15
|
-
#
|
|
15
|
+
# non-interactive:
|
|
16
16
|
vitrine init my-shop --yes --tier simple-store --backend payload --features catalog,product-page,seo,cart
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
(`payload` / `vendure`),
|
|
20
|
-
2.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
The wizard asks for the tier (`catalog` / `simple-store` / `full-store`), backend
|
|
19
|
+
(`payload` / `vendure`), the feature set, and the payment provider.
|
|
20
|
+
2. **Then drive it with an AI agent.** The generated repo includes `CLAUDE.md` (operational
|
|
21
|
+
guide + command reference), `AGENTS.md` (for cross-tool agents), and Claude Code slash commands
|
|
22
|
+
in `.claude/commands/`. Open the project in Claude Code and run `/setup` — the agent installs
|
|
23
|
+
dependencies, fills in `.env`, and starts the dev server. Then `/add-feature`, `/design`,
|
|
24
24
|
`/update`, `/doctor`.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Commands
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
`design apply`.
|
|
30
|
-
|
|
28
|
+
In the client repository: `init` (wizard), `add`, `remove`, `update`, `diff`, `list`, `doctor`,
|
|
29
|
+
`design apply`. Infrastructure: `kit update` / `kit status` / `self-update`. The full reference
|
|
30
|
+
with flags is in the generated project's `CLAUDE.md`.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
≡
|
|
34
|
-
`kit update`.
|
|
32
|
+
At its core is the **feature install primitive**, shared by `init` and `add` (guarantee: "added by
|
|
33
|
+
the wizard" ≡ "added later"). `add`/`update` work offline from the `~/.vitrine` cache; the network
|
|
34
|
+
is only needed for `kit update`.
|
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;
|
|
@@ -131,7 +131,7 @@ import { join as join3, resolve as resolve3 } from "path";
|
|
|
131
131
|
function vitrineHome() {
|
|
132
132
|
if (process.env.VITRINE_HOME) return resolve3(process.env.VITRINE_HOME);
|
|
133
133
|
const home = process.env.USERPROFILE ?? process.env.HOME;
|
|
134
|
-
if (!home) throw new Error("[vitrine]
|
|
134
|
+
if (!home) throw new Error("[vitrine] could not determine the home directory (HOME/USERPROFILE)");
|
|
135
135
|
return join3(home, ".vitrine");
|
|
136
136
|
}
|
|
137
137
|
function cachePaths(home = vitrineHome()) {
|
|
@@ -171,7 +171,7 @@ function computeChangelog(oldIndex, newIndex) {
|
|
|
171
171
|
return entries;
|
|
172
172
|
}
|
|
173
173
|
function formatChangelog(entries) {
|
|
174
|
-
if (entries.length === 0) return "
|
|
174
|
+
if (entries.length === 0) return "no changes to the feature set";
|
|
175
175
|
return entries.map((e) => {
|
|
176
176
|
if (e.kind === "added") return `+ ${e.name}${e.to ? ` ${e.to}` : ""}`;
|
|
177
177
|
if (e.kind === "removed") return `- ${e.name}`;
|
|
@@ -183,7 +183,7 @@ function populateCache(fromDir, opts = {}) {
|
|
|
183
183
|
const srcRegistry = join3(fromDir, "registry");
|
|
184
184
|
const srcTemplates = join3(fromDir, "templates");
|
|
185
185
|
if (!existsSync3(join3(srcRegistry, "_index.json"))) {
|
|
186
|
-
throw new Error(`[vitrine]
|
|
186
|
+
throw new Error(`[vitrine] "${fromDir}" has no registry/_index.json \u2014 not a kit tree`);
|
|
187
187
|
}
|
|
188
188
|
const paths = cachePaths(home);
|
|
189
189
|
const oldIndex = readIndex(paths.registry);
|
|
@@ -229,7 +229,7 @@ function resolveRegistryRoot(explicit) {
|
|
|
229
229
|
}
|
|
230
230
|
const dev = findUpRegistry(process.cwd());
|
|
231
231
|
if (dev) return dev;
|
|
232
|
-
throw new Error('[vitrine]
|
|
232
|
+
throw new Error('[vitrine] registry not found. Run "vitrine kit update" or pass --registry.');
|
|
233
233
|
}
|
|
234
234
|
function createRegistrySource(explicitRoot) {
|
|
235
235
|
const root = resolveRegistryRoot(explicitRoot);
|
|
@@ -240,7 +240,7 @@ function createRegistrySource(explicitRoot) {
|
|
|
240
240
|
const cached = cache.get(name);
|
|
241
241
|
if (cached) return cached;
|
|
242
242
|
const file = join4(featureDir(name), "feature.json");
|
|
243
|
-
if (!existsSync4(file)) throw new Error(`[vitrine]
|
|
243
|
+
if (!existsSync4(file)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
|
|
244
244
|
const manifest = featureManifestSchema.parse(JSON.parse(readText(file)));
|
|
245
245
|
cache.set(name, manifest);
|
|
246
246
|
return manifest;
|
|
@@ -326,7 +326,7 @@ function assertNoPascalCollisions(features) {
|
|
|
326
326
|
const id = pascalCase(f.name);
|
|
327
327
|
const prev = byPascal.get(id);
|
|
328
328
|
if (prev && prev !== f.name) {
|
|
329
|
-
throw new Error(`[vitrine]
|
|
329
|
+
throw new Error(`[vitrine] features "${prev}" and "${f.name}" produce the same identifier "${id}" \u2014 rename one`);
|
|
330
330
|
}
|
|
331
331
|
byPascal.set(id, f.name);
|
|
332
332
|
}
|
|
@@ -339,11 +339,11 @@ function renderSlotsFile(features) {
|
|
|
339
339
|
);
|
|
340
340
|
const calls = withSlots.map((f) => ` register${pascalCase(f.name)}Slots();`);
|
|
341
341
|
return [
|
|
342
|
-
"// vitrine:generated \u2014
|
|
342
|
+
"// vitrine:generated \u2014 slot registration for installed features. Do not edit by hand.",
|
|
343
343
|
...imports,
|
|
344
344
|
"",
|
|
345
345
|
"export function registerSlots(): void {",
|
|
346
|
-
...calls.length ? calls : [" //
|
|
346
|
+
...calls.length ? calls : [" // no slot features"],
|
|
347
347
|
"}",
|
|
348
348
|
""
|
|
349
349
|
].join("\n");
|
|
@@ -356,11 +356,11 @@ function renderPaymentsFile(features) {
|
|
|
356
356
|
);
|
|
357
357
|
const calls = withPayment.map((f) => ` register${pascalCase(f.name)}Provider();`);
|
|
358
358
|
return [
|
|
359
|
-
"// vitrine:generated \u2014
|
|
359
|
+
"// vitrine:generated \u2014 payment-provider registration for installed features. Do not edit by hand.",
|
|
360
360
|
...imports,
|
|
361
361
|
"",
|
|
362
362
|
"export function registerPayments(): void {",
|
|
363
|
-
...calls.length ? calls : [" //
|
|
363
|
+
...calls.length ? calls : [" // no payment features"],
|
|
364
364
|
"}",
|
|
365
365
|
""
|
|
366
366
|
].join("\n");
|
|
@@ -373,7 +373,7 @@ function renderBlueprintFile(features) {
|
|
|
373
373
|
);
|
|
374
374
|
const calls = withBp.map((f) => ` extend${pascalCase(f.name)}Blueprint(blueprint);`);
|
|
375
375
|
return [
|
|
376
|
-
"// vitrine:generated \u2014 blueprint
|
|
376
|
+
"// vitrine:generated \u2014 blueprint of installed features. Do not edit by hand.",
|
|
377
377
|
"import { createBlueprint } from '@vitrine-kit/payload-blueprint';",
|
|
378
378
|
...imports,
|
|
379
379
|
"",
|
|
@@ -385,9 +385,9 @@ function renderBlueprintFile(features) {
|
|
|
385
385
|
].join("\n");
|
|
386
386
|
}
|
|
387
387
|
function renderClaudeFeaturesTable(features) {
|
|
388
|
-
if (features.length === 0) return "
|
|
388
|
+
if (features.length === 0) return "_No features installed yet._";
|
|
389
389
|
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 ["|
|
|
390
|
+
return ["| Feature | Description | Version |", "|---|---|---|", rows].join("\n");
|
|
391
391
|
}
|
|
392
392
|
function mergeEnvExample(existing, features) {
|
|
393
393
|
const present = parseEnvKeys(existing);
|
|
@@ -439,7 +439,7 @@ function renderNeutralTheme() {
|
|
|
439
439
|
};
|
|
440
440
|
const lines = TOKEN_CSS_VARS.map((v) => ` ${v}: ${neutral[v] ?? "initial"};`).join("\n");
|
|
441
441
|
return [
|
|
442
|
-
"/* vitrine:
|
|
442
|
+
"/* vitrine: client token values. The design step (vitrine design apply) rewrites only this. */",
|
|
443
443
|
":root {",
|
|
444
444
|
lines,
|
|
445
445
|
"}",
|
|
@@ -467,7 +467,7 @@ function resolveOrder(names, registry) {
|
|
|
467
467
|
const visit = (name) => {
|
|
468
468
|
if (seen.has(name)) return;
|
|
469
469
|
if (!registry.hasFeature(name)) {
|
|
470
|
-
throw new Error(`[vitrine]
|
|
470
|
+
throw new Error(`[vitrine] feature "${name}" not found in the registry`);
|
|
471
471
|
}
|
|
472
472
|
seen.add(name);
|
|
473
473
|
const manifest = registry.loadManifest(name);
|
|
@@ -480,12 +480,12 @@ function resolveOrder(names, registry) {
|
|
|
480
480
|
function validate(name, manifest, project) {
|
|
481
481
|
if (!manifest.tier.includes(project.lock.tier)) {
|
|
482
482
|
throw new Error(
|
|
483
|
-
`[vitrine]
|
|
483
|
+
`[vitrine] feature "${name}" does not support tier "${project.lock.tier}" (only ${manifest.tier.join(", ")})`
|
|
484
484
|
);
|
|
485
485
|
}
|
|
486
486
|
for (const conflict of manifest.conflicts ?? []) {
|
|
487
487
|
if (project.lock.features[conflict]) {
|
|
488
|
-
throw new Error(`[vitrine]
|
|
488
|
+
throw new Error(`[vitrine] conflict: "${name}" is incompatible with the installed "${conflict}"`);
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
}
|
|
@@ -494,7 +494,7 @@ function copyFeatureFiles(project, name, manifest, registry, tx) {
|
|
|
494
494
|
const originalsBase = join6(projectPaths(project.root).originals, `${name}@${manifest.kitVersion}`);
|
|
495
495
|
for (const map of manifest.files) {
|
|
496
496
|
if (!exists(join6(featDir, map.from))) {
|
|
497
|
-
throw new Error(`[vitrine]
|
|
497
|
+
throw new Error(`[vitrine] feature "${name}": no source "${map.from}"`);
|
|
498
498
|
}
|
|
499
499
|
for (const file of eachFeatureFile(featDir, map)) {
|
|
500
500
|
const content = readText(file.srcAbs);
|
|
@@ -581,16 +581,16 @@ function installFeatures(project, names, registry) {
|
|
|
581
581
|
}
|
|
582
582
|
function removeFeature(project, name, registry) {
|
|
583
583
|
const removed = project.lock.features[name];
|
|
584
|
-
if (!removed) throw new Error(`[vitrine]
|
|
584
|
+
if (!removed) throw new Error(`[vitrine] feature "${name}" is not installed`);
|
|
585
585
|
const manifest = registry.loadManifest(name);
|
|
586
586
|
if (!manifest.removable) {
|
|
587
|
-
throw new Error(`[vitrine]
|
|
587
|
+
throw new Error(`[vitrine] feature "${name}" is not removable (removable: false)`);
|
|
588
588
|
}
|
|
589
589
|
for (const other of Object.keys(project.lock.features)) {
|
|
590
590
|
if (other === name) continue;
|
|
591
591
|
const deps = registry.loadManifest(other).registryDependencies ?? [];
|
|
592
592
|
if (deps.includes(name)) {
|
|
593
|
-
throw new Error(`[vitrine]
|
|
593
|
+
throw new Error(`[vitrine] cannot remove "${name}": "${other}" depends on it`);
|
|
594
594
|
}
|
|
595
595
|
}
|
|
596
596
|
const originalsDir = join6(projectPaths(project.root).originals, `${name}@${removed.version}`);
|
|
@@ -619,12 +619,12 @@ import { spawnSync } from "child_process";
|
|
|
619
619
|
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
|
|
620
620
|
import { delimiter, join as join7 } from "path";
|
|
621
621
|
import { TOKEN_CSS_VARS as TOKEN_CSS_VARS2 } from "@vitrine-kit/contracts";
|
|
622
|
-
var DESIGN_HEADING = "##
|
|
622
|
+
var DESIGN_HEADING = "## INSTRUCTION: apply the design from /design";
|
|
623
623
|
function findClaudeBin(explicit) {
|
|
624
624
|
const pinned = explicit ?? process.env.VITRINE_CLAUDE_BIN;
|
|
625
625
|
if (pinned) {
|
|
626
626
|
if (existsSync6(pinned)) return pinned;
|
|
627
|
-
throw new Error(`[vitrine] Claude Code
|
|
627
|
+
throw new Error(`[vitrine] Claude Code not found at path "${pinned}"`);
|
|
628
628
|
}
|
|
629
629
|
const names = process.platform === "win32" ? ["claude.cmd", "claude.exe", "claude.ps1", "claude"] : ["claude"];
|
|
630
630
|
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
@@ -634,7 +634,7 @@ function findClaudeBin(explicit) {
|
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
636
|
throw new Error(
|
|
637
|
-
"[vitrine] Claude Code CLI
|
|
637
|
+
"[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
638
|
);
|
|
639
639
|
}
|
|
640
640
|
function designHasInput(root) {
|
|
@@ -655,14 +655,14 @@ function buildDesignPrompt(project) {
|
|
|
655
655
|
return [
|
|
656
656
|
instruction,
|
|
657
657
|
"",
|
|
658
|
-
"
|
|
659
|
-
"-
|
|
660
|
-
"-
|
|
661
|
-
"-
|
|
658
|
+
"Application context:",
|
|
659
|
+
"- Design source: the `design/` folder (export from Claude Design).",
|
|
660
|
+
"- The only editable file: `theme/client.css` \u2014 set the CSS variable values.",
|
|
661
|
+
"- Closed set of token names (do not introduce others):",
|
|
662
662
|
tokens,
|
|
663
663
|
"",
|
|
664
|
-
"
|
|
665
|
-
"
|
|
664
|
+
"Do NOT edit: components, adapters, routes, site.config, lib/*. Only the variable",
|
|
665
|
+
"values in theme/client.css. Idempotency: re-running does not accumulate changes."
|
|
666
666
|
].join("\n");
|
|
667
667
|
}
|
|
668
668
|
var defaultRunner = ({ bin, args, cwd }) => {
|
|
@@ -679,12 +679,12 @@ function planDesignApply(project, opts = {}) {
|
|
|
679
679
|
function designApply(project, opts = {}, runner = defaultRunner) {
|
|
680
680
|
if (!designHasInput(project.root)) {
|
|
681
681
|
throw new Error(
|
|
682
|
-
"[vitrine]
|
|
682
|
+
"[vitrine] the design/ folder is empty \u2014 add an export from Claude Design and retry."
|
|
683
683
|
);
|
|
684
684
|
}
|
|
685
685
|
const cmd = planDesignApply(project, opts);
|
|
686
686
|
if (opts.dryRun) {
|
|
687
|
-
console.log(`[vitrine] dry-run: ${cmd.bin} -p
|
|
687
|
+
console.log(`[vitrine] dry-run: ${cmd.bin} -p <prompt ${cmd.prompt.length} chars> --permission-mode acceptEdits`);
|
|
688
688
|
return 0;
|
|
689
689
|
}
|
|
690
690
|
return runner(cmd);
|
|
@@ -704,21 +704,21 @@ function runDoctor(project, registry) {
|
|
|
704
704
|
const paymentsText = exists(paths.payments) ? readText(paths.payments) : "";
|
|
705
705
|
for (const core of ["@vitrine-kit/contracts", "@vitrine-kit/core"]) {
|
|
706
706
|
if (!deps[core]) {
|
|
707
|
-
add({ severity: "error", scope: "packages", message:
|
|
707
|
+
add({ severity: "error", scope: "packages", message: `missing dependency ${core}`, fix: "add it to package.json" });
|
|
708
708
|
}
|
|
709
709
|
}
|
|
710
|
-
if (exists(paths.claude) && !readText(paths.claude).includes("
|
|
710
|
+
if (exists(paths.claude) && !readText(paths.claude).includes("INSTRUCTION: apply the design")) {
|
|
711
711
|
add({
|
|
712
712
|
severity: "warn",
|
|
713
713
|
scope: "design",
|
|
714
|
-
message: "
|
|
715
|
-
fix: "
|
|
714
|
+
message: "CLAUDE.md has no design-instruction block",
|
|
715
|
+
fix: "update CLAUDE.md (kit update brings a fresh instruction)"
|
|
716
716
|
});
|
|
717
717
|
}
|
|
718
718
|
for (const [name, pin] of Object.entries(project.lock.features)) {
|
|
719
719
|
const scope = `feature:${name}`;
|
|
720
720
|
if (!registry.hasFeature(name)) {
|
|
721
|
-
add({ severity: "error", scope, message:
|
|
721
|
+
add({ severity: "error", scope, message: `feature not found in the registry`, fix: "vitrine kit update" });
|
|
722
722
|
continue;
|
|
723
723
|
}
|
|
724
724
|
const manifest = registry.loadManifest(name);
|
|
@@ -726,7 +726,7 @@ function runDoctor(project, registry) {
|
|
|
726
726
|
add({
|
|
727
727
|
severity: "warn",
|
|
728
728
|
scope,
|
|
729
|
-
message:
|
|
729
|
+
message: `repo version ${pin.version}, registry offers ${manifest.kitVersion}`,
|
|
730
730
|
fix: `vitrine update ${name}`
|
|
731
731
|
});
|
|
732
732
|
}
|
|
@@ -737,8 +737,8 @@ function runDoctor(project, registry) {
|
|
|
737
737
|
add({
|
|
738
738
|
severity: "error",
|
|
739
739
|
scope,
|
|
740
|
-
message:
|
|
741
|
-
fix: `vitrine add ${name} (
|
|
740
|
+
message: `missing file "${file.toRel}"`,
|
|
741
|
+
fix: `vitrine add ${name} (reinstalls)`
|
|
742
742
|
});
|
|
743
743
|
}
|
|
744
744
|
}
|
|
@@ -748,33 +748,33 @@ function runDoctor(project, registry) {
|
|
|
748
748
|
add({
|
|
749
749
|
severity: e.required ? "error" : "warn",
|
|
750
750
|
scope,
|
|
751
|
-
message:
|
|
752
|
-
fix: "
|
|
751
|
+
message: `missing env key "${e.key}"${e.required ? " (required)" : ""}`,
|
|
752
|
+
fix: "add it to .env.example/.env"
|
|
753
753
|
});
|
|
754
754
|
}
|
|
755
755
|
}
|
|
756
756
|
const need = [...Object.keys(manifest.corePackages ?? {}), ...(manifest.npm ?? []).map((s) => parseNpmSpec(s).name)];
|
|
757
757
|
for (const dep of need) {
|
|
758
758
|
if (!deps[dep]) {
|
|
759
|
-
add({ severity: "error", scope, message:
|
|
759
|
+
add({ severity: "error", scope, message: `missing dependency ${dep}`, fix: `vitrine add ${name} (merges deps)` });
|
|
760
760
|
}
|
|
761
761
|
}
|
|
762
762
|
if ((manifest.slots?.length ?? 0) > 0) {
|
|
763
763
|
const fn = `register${pascalCase(name)}Slots`;
|
|
764
764
|
if (!slotsText.includes(fn)) {
|
|
765
|
-
add({ severity: "error", scope, message: `lib/slots.ts
|
|
765
|
+
add({ severity: "error", scope, message: `lib/slots.ts does not call ${fn}`, fix: `vitrine add ${name} (regenerates slots)` });
|
|
766
766
|
}
|
|
767
767
|
}
|
|
768
768
|
if (!configText.includes(`"${name}": true`)) {
|
|
769
|
-
add({ severity: "warn", scope, message:
|
|
769
|
+
add({ severity: "warn", scope, message: `site.config has no features.${name} flag`, fix: `vitrine add ${name} (regenerates flags)` });
|
|
770
770
|
}
|
|
771
771
|
if (manifest.payment) {
|
|
772
772
|
const fn = `register${pascalCase(name)}Provider`;
|
|
773
773
|
if (!paymentsText.includes(fn)) {
|
|
774
|
-
add({ severity: "error", scope, message: `lib/payments.ts
|
|
774
|
+
add({ severity: "error", scope, message: `lib/payments.ts does not call ${fn}`, fix: `vitrine add ${name} (regenerates payments)` });
|
|
775
775
|
}
|
|
776
776
|
if (!configText.includes(`payments: ${JSON.stringify(manifest.payment.provider)}`)) {
|
|
777
|
-
add({ severity: "warn", scope, message:
|
|
777
|
+
add({ severity: "warn", scope, message: `site.config integrations.payments \u2260 "${manifest.payment.provider}"`, fix: `vitrine add ${name} (regenerates integrations)` });
|
|
778
778
|
}
|
|
779
779
|
}
|
|
780
780
|
}
|
|
@@ -851,8 +851,8 @@ function merge3(base, ours, theirs, labels = {}) {
|
|
|
851
851
|
const B = splitLines(base);
|
|
852
852
|
const O = splitLines(ours);
|
|
853
853
|
const T = splitLines(theirs);
|
|
854
|
-
const ourLabel = labels.ours ?? "ours (
|
|
855
|
-
const theirLabel = labels.theirs ?? "theirs (
|
|
854
|
+
const ourLabel = labels.ours ?? "ours (client repo)";
|
|
855
|
+
const theirLabel = labels.theirs ?? "theirs (registry)";
|
|
856
856
|
if (B.length * Math.max(O.length, T.length) > MAX_LCS_CELLS) {
|
|
857
857
|
if (ours === theirs || theirs === base) return { text: ours, clean: true, conflicts: 0 };
|
|
858
858
|
if (ours === base) return { text: theirs, clean: true, conflicts: 0 };
|
|
@@ -921,8 +921,8 @@ function merge3(base, ours, theirs, labels = {}) {
|
|
|
921
921
|
// src/update.ts
|
|
922
922
|
function planUpdate(project, name, registry) {
|
|
923
923
|
const pin = project.lock.features[name];
|
|
924
|
-
if (!pin) throw new Error(`[vitrine]
|
|
925
|
-
if (!registry.hasFeature(name)) throw new Error(`[vitrine]
|
|
924
|
+
if (!pin) throw new Error(`[vitrine] feature "${name}" is not installed`);
|
|
925
|
+
if (!registry.hasFeature(name)) throw new Error(`[vitrine] feature "${name}" not found in the registry`);
|
|
926
926
|
const manifest = registry.loadManifest(name);
|
|
927
927
|
const fromVersion = pin.version;
|
|
928
928
|
const toVersion = manifest.kitVersion;
|
|
@@ -987,17 +987,17 @@ function applyUpdate(project, plan, registry) {
|
|
|
987
987
|
function renderPlan(plan) {
|
|
988
988
|
const head = plan.fromVersion === plan.toVersion ? `${plan.feature} @ ${plan.toVersion}` : `${plan.feature} ${plan.fromVersion} \u2192 ${plan.toVersion}`;
|
|
989
989
|
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 ? ` (
|
|
990
|
+
const mark = f.status === "conflict" ? "\u2717 conflict" : f.status === "new" ? "+ new " : "~ merge ";
|
|
991
|
+
return ` ${mark} ${f.to}${f.conflicts ? ` (conflicts: ${f.conflicts})` : ""}`;
|
|
992
992
|
});
|
|
993
|
-
return lines.length === 0 ? `${head}:
|
|
993
|
+
return lines.length === 0 ? `${head}: no changes` : [head, ...lines].join("\n");
|
|
994
994
|
}
|
|
995
995
|
|
|
996
996
|
// src/commands.ts
|
|
997
997
|
function requireProject() {
|
|
998
998
|
const root = findProjectRoot();
|
|
999
999
|
if (!root) {
|
|
1000
|
-
throw new Error("[vitrine]
|
|
1000
|
+
throw new Error("[vitrine] vitrine.json not found \u2014 not a Vitrine client repository");
|
|
1001
1001
|
}
|
|
1002
1002
|
return loadProject(root);
|
|
1003
1003
|
}
|
|
@@ -1160,44 +1160,44 @@ function clientPackageJson(name, backend) {
|
|
|
1160
1160
|
function clientEnvExample(backend) {
|
|
1161
1161
|
if (backend === "vendure") {
|
|
1162
1162
|
return [
|
|
1163
|
-
"#
|
|
1163
|
+
"# Project environment.",
|
|
1164
1164
|
"",
|
|
1165
|
-
"#
|
|
1165
|
+
"# DB. Empty in dev \u2014 built-in SQLite (.vitrine/vendure.sqlite).",
|
|
1166
1166
|
"DATABASE_URL=",
|
|
1167
1167
|
"",
|
|
1168
|
-
"# Vendure Shop API (
|
|
1168
|
+
"# Vendure Shop API (storefront \u2192 server).",
|
|
1169
1169
|
"VENDURE_SHOP_API_URL=http://localhost:3001/shop-api",
|
|
1170
1170
|
"",
|
|
1171
|
-
"#
|
|
1171
|
+
"# Vendure superadmin (dev default superadmin/superadmin; change it for prod).",
|
|
1172
1172
|
"VENDURE_SUPERADMIN_USERNAME=",
|
|
1173
1173
|
"VENDURE_SUPERADMIN_PASSWORD=",
|
|
1174
1174
|
"VENDURE_COOKIE_SECRET=",
|
|
1175
1175
|
"",
|
|
1176
|
-
"#
|
|
1176
|
+
"# Storefront base URL.",
|
|
1177
1177
|
"NEXT_PUBLIC_SITE_URL=http://localhost:3000",
|
|
1178
1178
|
""
|
|
1179
1179
|
].join("\n");
|
|
1180
1180
|
}
|
|
1181
1181
|
if (backend !== "payload") {
|
|
1182
|
-
return "#
|
|
1182
|
+
return "# Project environment.\nDATABASE_URL=\n";
|
|
1183
1183
|
}
|
|
1184
1184
|
return [
|
|
1185
|
-
"#
|
|
1185
|
+
"# Project environment.",
|
|
1186
1186
|
"",
|
|
1187
|
-
"#
|
|
1187
|
+
"# DB. For local dev you can leave it empty \u2014 a SQLite fallback is used (.vitrine/dev.sqlite).",
|
|
1188
1188
|
"DATABASE_URL=",
|
|
1189
1189
|
"",
|
|
1190
|
-
"#
|
|
1190
|
+
"# Payload secret (required; generate a random one for prod).",
|
|
1191
1191
|
"PAYLOAD_SECRET=",
|
|
1192
1192
|
"",
|
|
1193
|
-
"# Dev
|
|
1193
|
+
"# Dev admin (created only in dev when the DB is empty; the password is printed to the console).",
|
|
1194
1194
|
"DEV_ADMIN_EMAIL=",
|
|
1195
1195
|
"DEV_ADMIN_PASSWORD=",
|
|
1196
1196
|
"",
|
|
1197
|
-
"#
|
|
1197
|
+
"# Site base URL (canonical, OG).",
|
|
1198
1198
|
"NEXT_PUBLIC_SITE_URL=http://localhost:3000",
|
|
1199
1199
|
"",
|
|
1200
|
-
"#
|
|
1200
|
+
"# Disable the SQLite fallback even in dev (to catch config typos):",
|
|
1201
1201
|
"# VITRINE_DB_STRICT=1",
|
|
1202
1202
|
""
|
|
1203
1203
|
].join("\n");
|
|
@@ -1207,13 +1207,13 @@ function clientReadme(name, backend, tier) {
|
|
|
1207
1207
|
"```bash",
|
|
1208
1208
|
"pnpm install",
|
|
1209
1209
|
"cp .env.example .env",
|
|
1210
|
-
"pnpm vendure # Vendure
|
|
1211
|
-
"pnpm dev #
|
|
1210
|
+
"pnpm vendure # Vendure server (Shop API on :3001) \u2014 in a separate terminal",
|
|
1211
|
+
"pnpm dev # storefront on :3000",
|
|
1212
1212
|
"```",
|
|
1213
1213
|
"",
|
|
1214
|
-
"
|
|
1215
|
-
"populate
|
|
1216
|
-
"superadmin/superadmin;
|
|
1214
|
+
"Without Postgres, dev starts a built-in SQLite (`.vitrine/vendure.sqlite`) and",
|
|
1215
|
+
"the populate seed. The superadmin comes from `VENDURE_SUPERADMIN_*` (dev default",
|
|
1216
|
+
"superadmin/superadmin; change it for prod)."
|
|
1217
1217
|
].join("\n") : [
|
|
1218
1218
|
"```bash",
|
|
1219
1219
|
"pnpm install",
|
|
@@ -1221,69 +1221,69 @@ function clientReadme(name, backend, tier) {
|
|
|
1221
1221
|
"pnpm dev",
|
|
1222
1222
|
"```",
|
|
1223
1223
|
"",
|
|
1224
|
-
"-
|
|
1225
|
-
"-
|
|
1224
|
+
"- Storefront: http://localhost:3000",
|
|
1225
|
+
"- Admin: http://localhost:3000/admin",
|
|
1226
1226
|
"",
|
|
1227
|
-
"
|
|
1228
|
-
"
|
|
1229
|
-
"
|
|
1227
|
+
"Without Postgres, dev starts a built-in SQLite (`.vitrine/dev.sqlite`), seeds a",
|
|
1228
|
+
"demo catalog (5 products, 2 categories) and creates a dev admin (login/password printed",
|
|
1229
|
+
"to the console once). Disable the fallback in dev too with `VITRINE_DB_STRICT=1`."
|
|
1230
1230
|
].join("\n");
|
|
1231
|
-
const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... #
|
|
1231
|
+
const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... # Vendure cookie secret" : "export PAYLOAD_SECRET=... # random Payload secret";
|
|
1232
1232
|
return `# ${name}
|
|
1233
1233
|
|
|
1234
|
-
|
|
1235
|
-
Next.js + Tailwind;
|
|
1236
|
-
|
|
1234
|
+
A client project on Vitrine. Backend: \`${backend}\`, tier: \`${tier}\`.
|
|
1235
|
+
Next.js + Tailwind; features are copied from the Vitrine registry \u2014 you own the code and
|
|
1236
|
+
style it with tokens (\`theme/client.css\`) without changing the logic.
|
|
1237
1237
|
|
|
1238
|
-
## 1.
|
|
1238
|
+
## 1. Prerequisites
|
|
1239
1239
|
|
|
1240
|
-
Node >= 20 (LTS)
|
|
1241
|
-
|
|
1240
|
+
Node >= 20 (LTS) and \`pnpm\`. The \`@vitrine-kit/*\` packages are public on npm \u2014
|
|
1241
|
+
no token is needed to install them.
|
|
1242
1242
|
|
|
1243
|
-
## 2.
|
|
1243
|
+
## 2. Local run (zero-config)
|
|
1244
1244
|
|
|
1245
1245
|
${run}
|
|
1246
1246
|
|
|
1247
|
-
## 3.
|
|
1247
|
+
## 3. Apply the client design
|
|
1248
1248
|
|
|
1249
|
-
1.
|
|
1250
|
-
2. \`vitrine design apply\` \u2014
|
|
1251
|
-
(
|
|
1249
|
+
1. Put the brand export (Figma export, screenshots, assets) in \`/design\`.
|
|
1250
|
+
2. \`vitrine design apply\` \u2014 the AI sets token values in \`theme/client.css\`
|
|
1251
|
+
(doesn't touch logic/data/routing/a11y). The step is idempotent.
|
|
1252
1252
|
|
|
1253
|
-
## 4.
|
|
1253
|
+
## 4. Features: add, remove, view
|
|
1254
1254
|
|
|
1255
1255
|
\`\`\`bash
|
|
1256
|
-
vitrine list #
|
|
1257
|
-
vitrine add reviews #
|
|
1258
|
-
vitrine remove reviews #
|
|
1259
|
-
vitrine design apply #
|
|
1256
|
+
vitrine list # installed + available
|
|
1257
|
+
vitrine add reviews # copy a feature: flag, slots, blueprint, env
|
|
1258
|
+
vitrine remove reviews # remove (if the feature is removable)
|
|
1259
|
+
vitrine design apply # style the new feature
|
|
1260
1260
|
\`\`\`
|
|
1261
1261
|
|
|
1262
|
-
\`add\`
|
|
1263
|
-
\`.vitrine/originals/\` \u2014
|
|
1262
|
+
\`add\` is idempotent and transactional (rollback on error); version originals are written to
|
|
1263
|
+
\`.vitrine/originals/\` \u2014 the basis for 3-way merge on update.
|
|
1264
1264
|
|
|
1265
|
-
## 5.
|
|
1265
|
+
## 5. Updates and checks
|
|
1266
1266
|
|
|
1267
1267
|
\`\`\`bash
|
|
1268
|
-
vitrine kit update #
|
|
1269
|
-
vitrine diff <feature> #
|
|
1270
|
-
vitrine update [feature] # 3-way merge
|
|
1271
|
-
vitrine doctor #
|
|
1268
|
+
vitrine kit update # update the local registry/templates cache from GitHub
|
|
1269
|
+
vitrine diff <feature> # preview a feature update
|
|
1270
|
+
vitrine update [feature] # 3-way merge of the new feature version (base = your snapshot)
|
|
1271
|
+
vitrine doctor # consistency: vitrine.json \u2194 files \u2194 packages \u2194 env
|
|
1272
1272
|
\`\`\`
|
|
1273
1273
|
|
|
1274
|
-
|
|
1275
|
-
\`@vitrine-kit/core\`,
|
|
1276
|
-
|
|
1274
|
+
The \`@vitrine-kit/*\` packages are versioned independently: a fix in \`core\` bumps only
|
|
1275
|
+
\`@vitrine-kit/core\`, while \`@vitrine-kit/contracts\` stays at its stable version \u2014
|
|
1276
|
+
update versions in \`package.json\` selectively.
|
|
1277
1277
|
|
|
1278
|
-
## 6.
|
|
1278
|
+
## 6. Deploy (VPS + Docker)
|
|
1279
1279
|
|
|
1280
1280
|
\`\`\`bash
|
|
1281
1281
|
${deploySecret}
|
|
1282
1282
|
docker compose up --build
|
|
1283
1283
|
\`\`\`
|
|
1284
1284
|
|
|
1285
|
-
Production
|
|
1286
|
-
(SQLite
|
|
1285
|
+
Production requires a real \`DATABASE_URL\` \u2014 without it the start aborts
|
|
1286
|
+
(the SQLite fallback is dev-only).
|
|
1287
1287
|
`;
|
|
1288
1288
|
}
|
|
1289
1289
|
function scaffoldBase(opts) {
|
|
@@ -1320,7 +1320,7 @@ export const siteConfig: SiteConfig = {
|
|
|
1320
1320
|
// vitrine:integrations:start
|
|
1321
1321
|
integrations: {},
|
|
1322
1322
|
// vitrine:integrations:end
|
|
1323
|
-
i18n: { defaultLocale: '
|
|
1323
|
+
i18n: { defaultLocale: 'en', locales: ['en'], currency: 'USD' },
|
|
1324
1324
|
};
|
|
1325
1325
|
|
|
1326
1326
|
export default siteConfig;
|
|
@@ -1330,67 +1330,67 @@ export default siteConfig;
|
|
|
1330
1330
|
join11(root, "CLAUDE.md"),
|
|
1331
1331
|
`# ${name}
|
|
1332
1332
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1333
|
+
A Vitrine project. Backend: \`${backend}\`, tier: \`${tier}\`.
|
|
1334
|
+
This file is the operational guide for the AI agent (Claude Code) and the developer. All
|
|
1335
|
+
starter-kit operations go through the \`vitrine\` CLI; ready-made flows are the slash commands in \`.claude/commands/\`.
|
|
1336
1336
|
|
|
1337
|
-
##
|
|
1337
|
+
## Installed features
|
|
1338
1338
|
<!-- vitrine:features:start -->
|
|
1339
|
-
|
|
1339
|
+
_No features installed yet._
|
|
1340
1340
|
<!-- vitrine:features:end -->
|
|
1341
1341
|
|
|
1342
|
-
##
|
|
1342
|
+
## vitrine CLI commands
|
|
1343
1343
|
|
|
1344
|
-
|
|
|
1344
|
+
| Command | Purpose | When to use | Flags |
|
|
1345
1345
|
|---|---|---|---|
|
|
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\` |
|
|
1346
|
+
| \`vitrine list\` | Installed and available features | before adding a feature | \u2014 |
|
|
1347
|
+
| \`vitrine add <features\u2026>\` | Copy feature(s): files, flag, slots, blueprint, env, deps | "add feature X" | \`--registry\` |
|
|
1348
|
+
| \`vitrine remove <feature>\` | Remove a feature (if \`removable\`) | "remove feature X" | \`--registry\` |
|
|
1349
|
+
| \`vitrine update [features\u2026]\` | Update features via 3-way merge (all if no arguments) | after \`kit update\` | \`--dry-run\`, \`--registry\` |
|
|
1350
|
+
| \`vitrine diff <feature>\` | Preview an update (without writing) | before \`update\` | \`--registry\` |
|
|
1351
|
+
| \`vitrine doctor\` | Consistency: \`vitrine.json\` \u2194 files \u2194 packages \u2194 env | after edits, when in doubt | \`--registry\` |
|
|
1352
|
+
| \`vitrine design apply\` | Apply the design from \`/design\` to tokens (via Claude Code) | after \`add\` or a rebrand | \`--bin\`, \`--dry-run\` |
|
|
1353
|
+
| \`vitrine kit update\` | Update the local registry/templates cache from GitHub | before updating features | \`--from\`, \`--version\`, \`--channel\` |
|
|
1354
|
+
| \`vitrine kit status\` | Cache version vs the CLI's expected one | diagnostics | \u2014 |
|
|
1355
|
+
| \`vitrine self-update\` | Update the CLI itself | rarely | \`--dry-run\` |
|
|
1356
1356
|
|
|
1357
|
-
\`init\`
|
|
1358
|
-
|
|
1359
|
-
\u2014
|
|
1357
|
+
\`init\` runs once when the repository is created (the \`vitrine init\` wizard). \`add\`/\`update\`
|
|
1358
|
+
are idempotent and transactional (rollback on error); version originals are written to \`.vitrine/originals/\`
|
|
1359
|
+
\u2014 the base for 3-way merge.
|
|
1360
1360
|
|
|
1361
|
-
##
|
|
1361
|
+
## Common scenarios
|
|
1362
1362
|
|
|
1363
|
-
-
|
|
1364
|
-
-
|
|
1365
|
-
-
|
|
1366
|
-
-
|
|
1367
|
-
-
|
|
1363
|
+
- **Project setup** \u2192 \`/setup\`: dependencies, \`.env\`, start the dev server.
|
|
1364
|
+
- **Add and style a feature** \u2192 \`/add-feature <name>\`: \`list\` \u2192 \`add\` \u2192 \`design apply\` \u2192 check.
|
|
1365
|
+
- **Apply/update the design** \u2192 \`/design\`: put the export in \`/design\`, \`design apply\`.
|
|
1366
|
+
- **Update features** \u2192 \`/update\`: \`kit update\` \u2192 \`diff\` \u2192 \`update\` \u2192 resolve conflicts \u2192 \`doctor\`.
|
|
1367
|
+
- **Check consistency** \u2192 \`/doctor\`.
|
|
1368
1368
|
|
|
1369
|
-
|
|
1369
|
+
The full human guide is in \`README.md\`.
|
|
1370
1370
|
|
|
1371
|
-
##
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
1)
|
|
1377
|
-
2)
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
a11y
|
|
1381
|
-
|
|
1382
|
-
(
|
|
1383
|
-
|
|
1371
|
+
## INSTRUCTION: apply the design from /design
|
|
1372
|
+
Input: everything in \`/design\`.
|
|
1373
|
+
Task: extract the visual language (palette, typography, spacing, radii, shadows,
|
|
1374
|
+
the look of specific components) and apply it to the project.
|
|
1375
|
+
Apply it like this:
|
|
1376
|
+
1) set token values in \`theme/client.css\` \u2014 the main lever;
|
|
1377
|
+
2) only if a token can't express what's needed \u2014 add presentational classes
|
|
1378
|
+
to a specific component WITHOUT changing its structure.
|
|
1379
|
+
Do NOT change: component logic, data fetching, adapter calls, routing,
|
|
1380
|
+
a11y roles/labels, public props. Tokens are the interface.
|
|
1381
|
+
If the design requires a different section structure \u2014 create a section override in the repo
|
|
1382
|
+
(composition) rather than editing the shared wireframe.
|
|
1383
|
+
The step is idempotent: re-running converges and doesn't accumulate cruft.
|
|
1384
1384
|
|
|
1385
|
-
##
|
|
1386
|
-
-
|
|
1387
|
-
\`lib/slots.ts\`, \`lib/payments.ts\`, \`lib/blueprint.ts\`,
|
|
1388
|
-
(\`features\`/\`integrations\`), \`vitrine.json\`,
|
|
1389
|
-
|
|
1390
|
-
-
|
|
1391
|
-
|
|
1392
|
-
-
|
|
1393
|
-
-
|
|
1385
|
+
## Boundaries (what the agent must not touch)
|
|
1386
|
+
- **Generated/managed files \u2014 do not edit by hand** (the CLI overwrites them from state):
|
|
1387
|
+
\`lib/slots.ts\`, \`lib/payments.ts\`, \`lib/blueprint.ts\`, the managed regions of \`site.config.ts\`
|
|
1388
|
+
(\`features\`/\`integrations\`), \`vitrine.json\`, the feature table in this \`CLAUDE.md\`, \`.env*\`.
|
|
1389
|
+
The set of features/integrations is changed via \`vitrine add/remove\`, not by editing files.
|
|
1390
|
+
- **Design \u2014 token values only** in \`theme/client.css\` (via \`vitrine design apply\`):
|
|
1391
|
+
don't change logic/data/routing/a11y/component structure.
|
|
1392
|
+
- **Contracts are extended additively** (\`@vitrine-kit/contracts\`): you must not break the shape of existing fields.
|
|
1393
|
+
- **The user makes commits** \u2014 don't run \`git commit\`/\`git push\` without an explicit request.
|
|
1394
1394
|
`
|
|
1395
1395
|
);
|
|
1396
1396
|
writeText(join11(root, "README.md"), clientReadme(name, backend, tier));
|
|
@@ -1403,7 +1403,7 @@ a11y-\u0440\u043E\u043B\u0438/\u043B\u0435\u0439\u0431\u043B\u044B, \u043F\u0443
|
|
|
1403
1403
|
}
|
|
1404
1404
|
function initProject(opts) {
|
|
1405
1405
|
if (exists(opts.root) && readdirSync3(opts.root).length > 0) {
|
|
1406
|
-
throw new Error(`[vitrine]
|
|
1406
|
+
throw new Error(`[vitrine] directory "${opts.root}" is not empty`);
|
|
1407
1407
|
}
|
|
1408
1408
|
scaffoldBase(opts);
|
|
1409
1409
|
const project = loadProject(opts.root);
|
|
@@ -1422,10 +1422,10 @@ function hasBin(bin) {
|
|
|
1422
1422
|
}
|
|
1423
1423
|
function acquireFromGh(version) {
|
|
1424
1424
|
if (!hasBin("gh")) {
|
|
1425
|
-
throw new Error("[vitrine]
|
|
1425
|
+
throw new Error("[vitrine] network update needs gh (GitHub CLI), or pass --from <dir>");
|
|
1426
1426
|
}
|
|
1427
1427
|
if (!hasBin("tar")) {
|
|
1428
|
-
throw new Error("[vitrine]
|
|
1428
|
+
throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
|
|
1429
1429
|
}
|
|
1430
1430
|
const tmp = mkdtempSync(join12(tmpdir(), "vitrine-kit-"));
|
|
1431
1431
|
const dl = spawnSync2(
|
|
@@ -1434,15 +1434,15 @@ function acquireFromGh(version) {
|
|
|
1434
1434
|
{ stdio: "inherit" }
|
|
1435
1435
|
);
|
|
1436
1436
|
if (dl.status !== 0) {
|
|
1437
|
-
throw new Error("[vitrine] gh release download
|
|
1437
|
+
throw new Error("[vitrine] gh release download failed (check access/auth: gh auth status, and gh auth login if needed)");
|
|
1438
1438
|
}
|
|
1439
1439
|
const tarball = readdirSync4(tmp).find((f) => f.endsWith(".tar.gz"));
|
|
1440
|
-
if (!tarball) throw new Error("[vitrine] release
|
|
1440
|
+
if (!tarball) throw new Error("[vitrine] release tarball not found after download");
|
|
1441
1441
|
if (spawnSync2("tar", ["-xzf", join12(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
|
|
1442
|
-
throw new Error("[vitrine]
|
|
1442
|
+
throw new Error("[vitrine] tarball extraction failed");
|
|
1443
1443
|
}
|
|
1444
1444
|
const root = readdirSync4(tmp).map((f) => join12(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync8(join12(p2, "registry", "_index.json")));
|
|
1445
|
-
if (!root) throw new Error("[vitrine]
|
|
1445
|
+
if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
|
|
1446
1446
|
return root;
|
|
1447
1447
|
}
|
|
1448
1448
|
function kitUpdate(opts = {}) {
|
|
@@ -1477,93 +1477,93 @@ function selfUpdate(opts = {}) {
|
|
|
1477
1477
|
var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
|
|
1478
1478
|
var program = new Command();
|
|
1479
1479
|
program.name("vitrine").description("Vitrine CLI").version(pkg.version);
|
|
1480
|
-
program.command("add").description("
|
|
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
1481
|
const res = addFeatures(features, opts.registry);
|
|
1482
|
-
console.log(res.installed.length ?
|
|
1482
|
+
console.log(res.installed.length ? `Installed: ${res.installed.join(", ")}` : "Already installed \u2014 no changes.");
|
|
1483
1483
|
});
|
|
1484
|
-
program.command("remove").description("
|
|
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
1485
|
removeFeatureCmd(feature, opts.registry);
|
|
1486
|
-
console.log(
|
|
1486
|
+
console.log(`Removed: ${feature}`);
|
|
1487
1487
|
});
|
|
1488
|
-
program.command("update").description("
|
|
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
1489
|
const outcomes = updateFeaturesCmd(features, opts.registry, { dryRun: opts.dryRun });
|
|
1490
1490
|
let conflicts = 0;
|
|
1491
1491
|
for (const { plan, applied } of outcomes) {
|
|
1492
1492
|
console.log(renderPlan(plan));
|
|
1493
|
-
if (applied) console.log(" \u2192
|
|
1493
|
+
if (applied) console.log(" \u2192 applied");
|
|
1494
1494
|
conflicts += plan.files.reduce((n, f) => n + f.conflicts, 0);
|
|
1495
1495
|
}
|
|
1496
1496
|
if (conflicts > 0) {
|
|
1497
1497
|
console.log(`
|
|
1498
|
-
\u26A0
|
|
1498
|
+
\u26A0 conflicts: ${conflicts}. Resolve the git markers (<<<<<<< / >>>>>>>) by hand.`);
|
|
1499
1499
|
}
|
|
1500
1500
|
});
|
|
1501
|
-
program.command("diff").description("
|
|
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
1502
|
console.log(renderPlan(diffFeatureCmd(feature, opts.registry)));
|
|
1503
1503
|
});
|
|
1504
|
-
program.command("list").description("
|
|
1504
|
+
program.command("list").description("Installed and available features").option("--registry <path>", "path to the registry").action((opts) => {
|
|
1505
1505
|
const { installed, available } = listFeatures(opts.registry);
|
|
1506
|
-
console.log("
|
|
1507
|
-
console.log("
|
|
1506
|
+
console.log("Installed:", installed.join(", ") || "\u2014");
|
|
1507
|
+
console.log("Available:", available.join(", ") || "\u2014");
|
|
1508
1508
|
});
|
|
1509
|
-
var design = program.command("design").description("
|
|
1510
|
-
design.command("apply").description("
|
|
1509
|
+
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
1511
|
const code = designApplyCmd({ bin: opts.bin, dryRun: opts.dryRun });
|
|
1512
1512
|
if (code !== 0) process.exit(code);
|
|
1513
1513
|
});
|
|
1514
|
-
var kit = program.command("kit").description("
|
|
1515
|
-
kit.command("update").description("
|
|
1514
|
+
var kit = program.command("kit").description("Local tooling (the ~/.vitrine registry cache)");
|
|
1515
|
+
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
1516
|
const res = kitUpdate({ from: opts.from, version: opts.version, channel: opts.channel });
|
|
1517
|
-
console.log(
|
|
1517
|
+
console.log(`Cache updated to kit ${res.kitVersion}.`);
|
|
1518
1518
|
console.log(formatChangelog(res.changelog));
|
|
1519
1519
|
});
|
|
1520
|
-
kit.command("status").description("
|
|
1520
|
+
kit.command("status").description("Cache version and what is newer than the installed CLI").action(() => {
|
|
1521
1521
|
const s = kitStatus();
|
|
1522
1522
|
if (!s.cached) {
|
|
1523
|
-
console.log('
|
|
1523
|
+
console.log('Cache is empty. Run "vitrine kit update".');
|
|
1524
1524
|
return;
|
|
1525
1525
|
}
|
|
1526
|
-
console.log(
|
|
1527
|
-
console.log(`CLI
|
|
1526
|
+
console.log(`Cache: kit ${s.kitVersion} (${s.channel}), features: ${s.featureCount ?? "\u2014"}, updated ${s.updatedAt}.`);
|
|
1527
|
+
console.log(`CLI expects kit ${s.cliKitVersion}.`);
|
|
1528
1528
|
});
|
|
1529
|
-
program.command("self-update").description("
|
|
1529
|
+
program.command("self-update").description("Update the CLI itself (@vitrine-kit/vitrine)").option("--dry-run", "show the command without running").action((opts) => {
|
|
1530
1530
|
const code = selfUpdate({ dryRun: opts.dryRun });
|
|
1531
1531
|
if (code !== 0) process.exit(code);
|
|
1532
1532
|
});
|
|
1533
|
-
program.command("doctor").description("
|
|
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
1534
|
const report = doctorCmd(opts.registry);
|
|
1535
1535
|
if (report.issues.length === 0) {
|
|
1536
|
-
console.log("\u2713
|
|
1536
|
+
console.log("\u2713 No issues found.");
|
|
1537
1537
|
return;
|
|
1538
1538
|
}
|
|
1539
1539
|
for (const i of report.issues) {
|
|
1540
|
-
const tag = i.severity === "error" ? "
|
|
1540
|
+
const tag = i.severity === "error" ? "ERROR" : "warn";
|
|
1541
1541
|
console.log(`[${tag}] ${i.scope}: ${i.message}${i.fix ? ` \u2192 ${i.fix}` : ""}`);
|
|
1542
1542
|
}
|
|
1543
1543
|
if (!report.ok) process.exit(1);
|
|
1544
1544
|
});
|
|
1545
|
-
program.command("init").description("
|
|
1545
|
+
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
1546
|
const registry = createRegistrySource(opts.registry);
|
|
1547
1547
|
let name = nameArg;
|
|
1548
1548
|
let tier = opts.tier;
|
|
1549
1549
|
let features = opts.features ? String(opts.features).split(",") : void 0;
|
|
1550
1550
|
if (!opts.yes && (!name || !tier)) {
|
|
1551
1551
|
p.intro("vitrine init");
|
|
1552
|
-
if (!name) name = String(await p.text({ message: "
|
|
1552
|
+
if (!name) name = String(await p.text({ message: "Project name", placeholder: "my-shop" }));
|
|
1553
1553
|
if (!tier) {
|
|
1554
1554
|
tier = await p.select({
|
|
1555
|
-
message: "
|
|
1555
|
+
message: "Tier",
|
|
1556
1556
|
options: [
|
|
1557
|
-
{ value: "catalog", label: "
|
|
1558
|
-
{ value: "simple-store", label: "
|
|
1559
|
-
{ value: "full-store", label: "
|
|
1557
|
+
{ value: "catalog", label: "Catalog" },
|
|
1558
|
+
{ value: "simple-store", label: "Simple store" },
|
|
1559
|
+
{ value: "full-store", label: "Full store" }
|
|
1560
1560
|
]
|
|
1561
1561
|
});
|
|
1562
1562
|
}
|
|
1563
1563
|
const suggested = suggestFeatures(tier, registry);
|
|
1564
1564
|
const baseline = suggested.filter((f) => !PAYMENT_PROVIDER_FEATURES.includes(f));
|
|
1565
1565
|
const picked = await p.multiselect({
|
|
1566
|
-
message: "
|
|
1566
|
+
message: "Features",
|
|
1567
1567
|
options: baseline.map((f) => ({ value: f, label: f })),
|
|
1568
1568
|
initialValues: baseline,
|
|
1569
1569
|
required: false
|
|
@@ -1574,14 +1574,14 @@ program.command("init").description("\u0421\u043E\u0437\u0434\u0430\u0442\u044C
|
|
|
1574
1574
|
const labels = {
|
|
1575
1575
|
"checkout-stripe": "Stripe",
|
|
1576
1576
|
"checkout-paddle": "Paddle",
|
|
1577
|
-
"checkout-yookassa": "
|
|
1577
|
+
"checkout-yookassa": "YooKassa"
|
|
1578
1578
|
};
|
|
1579
1579
|
const available = PAYMENT_PROVIDER_FEATURES.filter((f) => registry.hasFeature(f));
|
|
1580
1580
|
if (available.length > 0) {
|
|
1581
1581
|
const provider = await p.select({
|
|
1582
|
-
message: "
|
|
1582
|
+
message: "Payment provider",
|
|
1583
1583
|
options: [
|
|
1584
|
-
{ value: "none", label: "
|
|
1584
|
+
{ value: "none", label: "None (add later: vitrine add checkout-<provider>)" },
|
|
1585
1585
|
...available.map((f) => ({ value: f, label: labels[f] ?? f }))
|
|
1586
1586
|
],
|
|
1587
1587
|
initialValue: available[0]
|
|
@@ -1589,16 +1589,16 @@ program.command("init").description("\u0421\u043E\u0437\u0434\u0430\u0442\u044C
|
|
|
1589
1589
|
if (typeof provider === "string" && provider !== "none") features.push(provider);
|
|
1590
1590
|
}
|
|
1591
1591
|
}
|
|
1592
|
-
p.outro("
|
|
1592
|
+
p.outro("Ready");
|
|
1593
1593
|
}
|
|
1594
|
-
if (!name) throw new Error("[vitrine]
|
|
1594
|
+
if (!name) throw new Error("[vitrine] a project name is required");
|
|
1595
1595
|
const finalTier = tier ?? "catalog";
|
|
1596
1596
|
const backend = opts.backend ?? defaultBackend(finalTier);
|
|
1597
1597
|
const finalFeatures = features ?? suggestFeatures(finalTier, registry, backend);
|
|
1598
1598
|
const root = resolve6(String(opts.dir ?? process.cwd()), name);
|
|
1599
1599
|
const res = initProject({ root, name, backend, tier: finalTier, features: finalFeatures, registry });
|
|
1600
|
-
console.log(
|
|
1601
|
-
console.log(
|
|
1600
|
+
console.log(`Created project "${name}" \u2192 ${root}`);
|
|
1601
|
+
console.log(`Installed: ${res.installed.join(", ") || "\u2014"}`);
|
|
1602
1602
|
});
|
|
1603
1603
|
async function main() {
|
|
1604
1604
|
preflightNode();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitrine-kit/vitrine",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Vitrine CLI —
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Vitrine CLI — feature install primitive, init (wizard), add, update, doctor, design apply.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@clack/prompts": "^0.8.2",
|
|
23
23
|
"commander": "^12.1.0",
|
|
24
|
-
"@vitrine-kit/contracts": "1.
|
|
24
|
+
"@vitrine-kit/contracts": "1.2.0"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsup src/index.ts --format esm --clean",
|