@zenginui/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/check.d.ts +38 -0
- package/dist/check.js +92 -0
- package/dist/format-cli.d.ts +6 -0
- package/dist/format-cli.js +65 -0
- package/dist/git.d.ts +4 -0
- package/dist/git.js +39 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +463 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +56 -0
- package/dist/report.d.ts +28 -0
- package/dist/report.js +97 -0
- package/dist/scaffold.d.ts +54 -0
- package/dist/scaffold.js +322 -0
- package/package.json +56 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type BrandRadius } from "@zenginui/registry";
|
|
2
|
+
export interface ScaffoldOptions {
|
|
3
|
+
registry?: string;
|
|
4
|
+
template?: string;
|
|
5
|
+
theme?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
storybook: boolean;
|
|
8
|
+
local?: string;
|
|
9
|
+
force: boolean;
|
|
10
|
+
out?: string;
|
|
11
|
+
root?: string;
|
|
12
|
+
dir?: string;
|
|
13
|
+
list: boolean;
|
|
14
|
+
logo?: string;
|
|
15
|
+
primary?: string;
|
|
16
|
+
fontDisplay?: string;
|
|
17
|
+
fontSans?: string;
|
|
18
|
+
/** A registry pairing name, for brand. */
|
|
19
|
+
fonts?: string;
|
|
20
|
+
/** fonts: download the files into public/fonts instead of linking Google Fonts. */
|
|
21
|
+
selfHost?: boolean;
|
|
22
|
+
/** create: vite (default) or next. */
|
|
23
|
+
framework?: "vite" | "next";
|
|
24
|
+
fontMono?: string;
|
|
25
|
+
radius?: BrandRadius;
|
|
26
|
+
write: boolean;
|
|
27
|
+
map?: string;
|
|
28
|
+
collection?: string;
|
|
29
|
+
schema?: string;
|
|
30
|
+
count?: number;
|
|
31
|
+
seed?: number;
|
|
32
|
+
}
|
|
33
|
+
/** `zengin theme [name]`: list the registry's themes, or apply one to the current project. */
|
|
34
|
+
export declare function runTheme(name: string | undefined, opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
35
|
+
/** `zengin fonts [name] [--self-host]`: list the registry's pairings, or set this project's three font tokens to one. */
|
|
36
|
+
export declare function runFonts(name: string | undefined, opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
37
|
+
/** `zengin upgrade [items] [--write] [--force]`: what changed upstream since the components were copied, and take it. */
|
|
38
|
+
export declare function runUpgrade(names: string[], opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
39
|
+
/** `zengin icons [set]`: list the registry's icon sets, or point this project's icon vocabulary at one. */
|
|
40
|
+
export declare function runIcons(name: string | undefined, opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
41
|
+
/** `zengin brand --name <name> [--logo] [--primary] [--font-*] [--radius]`: a brand from one color. */
|
|
42
|
+
export declare function runBrand(opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
43
|
+
/** `zengin create <dir>`: a new project on the registry's components, checked by the engine before it prints. */
|
|
44
|
+
export declare function runCreate(dirArg: string | undefined, opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
45
|
+
/** `zengin add <items...>`: registry items into the current project, with manifest, styles and barrel updated. */
|
|
46
|
+
export declare function runAdd(names: string[], opts: ScaffoldOptions, cwd: string): Promise<string>;
|
|
47
|
+
/** `zengin tokens`: zengin/tokens*.json to src/styles/generated/tokens.css. */
|
|
48
|
+
export declare function runTokens(opts: ScaffoldOptions, cwd: string): string;
|
|
49
|
+
/** `zengin registry build`: the registry from a Zengin repository checkout, as static files. */
|
|
50
|
+
export declare function runRegistryBuild(opts: ScaffoldOptions, cwd: string): string;
|
|
51
|
+
/** `zengin figma export|import|connect|plugin`: tokens to variables and back, Code Connect files, the plugin. */
|
|
52
|
+
export declare function runFigma(sub: string | undefined, args: string[], opts: ScaffoldOptions, cwd: string): string;
|
|
53
|
+
/** `zengin mock <presets...> | --schema file`: typed, seeded fixture modules into src/mock. */
|
|
54
|
+
export declare function runMock(names: string[], opts: ScaffoldOptions, cwd: string): string;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { codeConnectFiles, fromFigmaVariables, renderImportReport, toFigmaVariables, writePlugin } from "@zenginui/figma";
|
|
4
|
+
import { generateMock, PRESETS, schemaFromPresets } from "@zenginui/mock";
|
|
5
|
+
import { applyFonts, applyIcons, applyTheme, applyUpgrade, planUpgrade, brandProject, buildRegistry, createProject, installItems, LAYOUT, listThemes, openRegistry, resolveItems, writeRegistry, writeTokensCss, listFonts, listIconSets } from "@zenginui/registry";
|
|
6
|
+
/** `zengin theme [name]`: list the registry's themes, or apply one to the current project. */
|
|
7
|
+
export async function runTheme(name, opts, cwd) {
|
|
8
|
+
const source = openRegistry(opts.registry);
|
|
9
|
+
if (!name || opts.list) {
|
|
10
|
+
const themes = await listThemes(source);
|
|
11
|
+
const width = Math.max(...themes.map((t) => t.name.length));
|
|
12
|
+
return [`Themes in ${source.location}:`, ...themes.map((t) => ` ${t.name.padEnd(width)} ${t.description}${t.fonts.length ? ` Fonts: ${t.fonts.join(", ")}.` : ""}`), "", "Apply one: zengin theme <name>"].join("\n");
|
|
13
|
+
}
|
|
14
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
15
|
+
const r = await applyTheme({ projectDir, name, source });
|
|
16
|
+
const lines = [`Applied the ${r.name} theme from ${source.location}.`, ...r.files.map((f) => ` wrote ${f}`)];
|
|
17
|
+
if (r.fonts.length)
|
|
18
|
+
lines.push(` fonts ${r.fonts.join(", ")}${r.html ? " (linked in index.html)" : " (no index.html to link them in)"}`);
|
|
19
|
+
lines.push("", "Every component wears it now. Run the dev server, or zengin brand to make it yours.");
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
/** `zengin fonts [name] [--self-host]`: list the registry's pairings, or set this project's three font tokens to one. */
|
|
23
|
+
export async function runFonts(name, opts, cwd) {
|
|
24
|
+
const source = openRegistry(opts.registry);
|
|
25
|
+
if (!name || opts.list) {
|
|
26
|
+
const pairings = await listFonts(source);
|
|
27
|
+
const width = Math.max(...pairings.map((p) => p.name.length));
|
|
28
|
+
return [
|
|
29
|
+
`Pairings in ${source.location}:`,
|
|
30
|
+
...pairings.map((p) => ` ${p.name.padEnd(width)} ${p.pairing.display.family} / ${p.pairing.sans.family} / ${p.pairing.mono.family}. ${p.description}`),
|
|
31
|
+
"",
|
|
32
|
+
"Apply one: zengin fonts <name> (--self-host downloads the files into public/fonts)",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
36
|
+
const r = await applyFonts({ projectDir, name, source, selfHost: opts.selfHost });
|
|
37
|
+
const lines = [`Set the ${r.name} pairing: ${r.pairing.display.family} for headlines, ${r.pairing.sans.family} for text, ${r.pairing.mono.family} for code.`, ...r.files.map((f) => ` wrote ${f}`)];
|
|
38
|
+
if (r.downloaded.length)
|
|
39
|
+
lines.push(` fonts ${r.downloaded.length} files in public/fonts, served from this project`);
|
|
40
|
+
else
|
|
41
|
+
lines.push(r.html ? " fonts linked from Google Fonts in index.html" : " fonts no index.html to link them in; add the Google Fonts link yourself or pass --self-host");
|
|
42
|
+
lines.push("", "The palette, radii and shadows are untouched. zengin theme replaces all of it; zengin brand starts over from a color.");
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
/** `zengin upgrade [items] [--write] [--force]`: what changed upstream since the components were copied, and take it. */
|
|
46
|
+
export async function runUpgrade(names, opts, cwd) {
|
|
47
|
+
const source = openRegistry(opts.registry);
|
|
48
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
49
|
+
const plan = await planUpgrade({ projectDir, source, ...(names.length ? { only: names } : {}) });
|
|
50
|
+
const counts = {};
|
|
51
|
+
for (const e of plan.entries)
|
|
52
|
+
counts[e.state] = (counts[e.state] ?? 0) + 1;
|
|
53
|
+
const lines = [`Project pins ${plan.projectVersion ?? "no version"}; ${source.location} is at ${plan.version}. ${plan.entries.length} owned files in ${plan.items.length} items.`];
|
|
54
|
+
const word = { current: "current ", upstream: "upstream", local: "local ", conflict: "CONFLICT", unknown: "unknown ", gone: "gone " };
|
|
55
|
+
for (const e of plan.entries)
|
|
56
|
+
if (e.state !== "current")
|
|
57
|
+
lines.push(` ${word[e.state]} ${e.path}${e.from ? ` (from ${e.from})` : ""}`);
|
|
58
|
+
if (!plan.entries.some((e) => e.state !== "current"))
|
|
59
|
+
lines.push(" Everything is what the registry ships.");
|
|
60
|
+
const summary = Object.entries(counts).map(([k, n]) => `${n} ${k}`).join(", ");
|
|
61
|
+
if (!opts.write) {
|
|
62
|
+
for (const e of plan.entries)
|
|
63
|
+
if (e.diff && (e.state === "conflict" || e.state === "unknown"))
|
|
64
|
+
lines.push("", `${e.path}:`, ...e.diff.split("\n").map((l) => ` ${l}`));
|
|
65
|
+
lines.push("", `${summary}. This was a report; zengin upgrade --write takes every upstream change. A conflict needs a merge, or --force to take upstream as is.`);
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
const r = await applyUpgrade(plan, { projectDir, source, force: opts.force });
|
|
69
|
+
for (const p of r.written)
|
|
70
|
+
lines.push(` wrote ${p}`);
|
|
71
|
+
for (const e of r.skipped)
|
|
72
|
+
lines.push(` held ${e.path} (${e.state}; merge by hand, or --force)`);
|
|
73
|
+
if (r.versionBumped)
|
|
74
|
+
lines.push(` pinned zengin.config.yaml now says ${plan.version}`);
|
|
75
|
+
lines.push("", `${summary}. ${r.written.length} written, ${r.skipped.length} held. Run zengin check to confirm the project is still clean.`);
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
/** `zengin icons [set]`: list the registry's icon sets, or point this project's icon vocabulary at one. */
|
|
79
|
+
export async function runIcons(name, opts, cwd) {
|
|
80
|
+
const source = openRegistry(opts.registry);
|
|
81
|
+
if (!name || opts.list) {
|
|
82
|
+
const sets = await listIconSets(source);
|
|
83
|
+
const width = Math.max(...sets.map((s) => s.name.length));
|
|
84
|
+
return [`Icon sets in ${source.location}:`, ...sets.map((s) => ` ${s.name.padEnd(width)} ${s.description} (${s.module})`), "", "Apply one: zengin icons <set> The names stay: <Icon.Search /> draws from the set you pick."].join("\n");
|
|
85
|
+
}
|
|
86
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
87
|
+
const r = await applyIcons({ projectDir, name, source });
|
|
88
|
+
const lines = [`Icons from ${r.name} (${r.module}).`, ...r.files.map((f) => ` ${r.replaced ? "rewrote" : "wrote "} ${f}`)];
|
|
89
|
+
const deps = Object.entries(r.dependencies).map(([k, v]) => `${k}@${v}`);
|
|
90
|
+
if (deps.length)
|
|
91
|
+
lines.push(` needs ${deps.join(", ")}: run npm install (or pnpm install)`);
|
|
92
|
+
lines.push("", "Every <Icon.Name /> in the project now draws from the set. Direct imports from an icon package are a component-substitution violation.");
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
/** `zengin brand --name <name> [--logo] [--primary] [--font-*] [--radius]`: a brand from one color. */
|
|
96
|
+
export async function runBrand(opts, cwd) {
|
|
97
|
+
if (!opts.name)
|
|
98
|
+
throw new Error("zengin brand needs --name <product name>. Optional: --logo <file>, --primary <hex>, --font-display, --font-sans, --font-mono, --radius sharp|soft|round.");
|
|
99
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
100
|
+
let { fontDisplay, fontSans, fontMono } = opts;
|
|
101
|
+
if (opts.fonts) {
|
|
102
|
+
// A pairing name from the registry stands in for the three families.
|
|
103
|
+
const pairing = (await listFonts(openRegistry(opts.registry))).find((p) => p.name === opts.fonts);
|
|
104
|
+
if (!pairing)
|
|
105
|
+
throw new Error(`No pairing "${opts.fonts}". List them with: zengin fonts`);
|
|
106
|
+
fontDisplay ??= pairing.pairing.display.family;
|
|
107
|
+
fontSans ??= pairing.pairing.sans.family;
|
|
108
|
+
fontMono ??= pairing.pairing.mono.family;
|
|
109
|
+
}
|
|
110
|
+
const r = await brandProject({ projectDir, name: opts.name, logo: opts.logo, primary: opts.primary, fontDisplay, fontSans, fontMono, radius: opts.radius });
|
|
111
|
+
const source = { option: "from --primary", logo: "from the logo", system: "the system default" }[r.primarySource];
|
|
112
|
+
const lines = [`Branded ${r.name}: primary ${r.primary} (${source}).`, ...r.files.map((f) => ` wrote ${f}`), "", "Contrast:"];
|
|
113
|
+
for (const c of r.contrast)
|
|
114
|
+
lines.push(` ${c.ratio.toFixed(2).padStart(5)} ${c.pair}`);
|
|
115
|
+
for (const w of r.warnings)
|
|
116
|
+
lines.push("", `Note: ${w}`);
|
|
117
|
+
lines.push("", `zengin check: ${r.violations} violations. Use <BrandMark /> from src/components/brand-mark.tsx for the wordmark.`);
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
/** `zengin create <dir>`: a new project on the registry's components, checked by the engine before it prints. */
|
|
121
|
+
export async function runCreate(dirArg, opts, cwd) {
|
|
122
|
+
if (!dirArg)
|
|
123
|
+
throw new Error("zengin create needs a directory: zengin create my-app [--template marketing]");
|
|
124
|
+
const source = openRegistry(opts.registry);
|
|
125
|
+
const r = await createProject({ dir: resolve(cwd, dirArg), name: opts.name, template: opts.template, theme: opts.theme, source, storybook: opts.storybook, local: opts.local, framework: opts.framework });
|
|
126
|
+
const lines = [
|
|
127
|
+
`Created ${r.name} in ${relative(cwd, r.dir) || "."} from the ${r.template} template${opts.theme ? ` with the ${opts.theme} theme` : ""}${r.framework === "next" ? " on Next.js" : ""} (Zengin UI ${r.version}, registry ${source.location}).`,
|
|
128
|
+
` ${r.install.components.length} components in ${LAYOUT.componentsDir}: ${r.install.components.join(", ")}`,
|
|
129
|
+
` tokens.css: ${r.tokens.light} tokens, ${r.tokens.dark} dark overrides`,
|
|
130
|
+
` zengin check: ${r.violations} violations`,
|
|
131
|
+
"",
|
|
132
|
+
"Next:",
|
|
133
|
+
` cd ${relative(cwd, r.dir) || "."}`,
|
|
134
|
+
" npm install # or pnpm install",
|
|
135
|
+
" npm run dev # the app",
|
|
136
|
+
...(opts.storybook ? [" npm run storybook # every component, both themes"] : []),
|
|
137
|
+
" npm run add -- select switch # more components from the registry",
|
|
138
|
+
];
|
|
139
|
+
if (r.violations > 0)
|
|
140
|
+
lines.push("", "The fresh project has violations, which means the registry is wrong. Run `zengin check` in it and report the output.");
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
/** `zengin add <items...>`: registry items into the current project, with manifest, styles and barrel updated. */
|
|
144
|
+
export async function runAdd(names, opts, cwd) {
|
|
145
|
+
if (!names.length)
|
|
146
|
+
throw new Error("zengin add needs at least one item: zengin add select switch");
|
|
147
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
148
|
+
if (!existsSync(join(projectDir, "zengin.config.yaml"))) {
|
|
149
|
+
throw new Error(`${projectDir} has no zengin.config.yaml. Run zengin add inside a project made by zengin create, or pass --dir.`);
|
|
150
|
+
}
|
|
151
|
+
const source = openRegistry(opts.registry);
|
|
152
|
+
const index = await source.index();
|
|
153
|
+
const items = await resolveItems(source, names);
|
|
154
|
+
const r = installItems({ projectDir, items, version: index.version, force: opts.force });
|
|
155
|
+
const missing = missingDependencies(projectDir, { ...r.dependencies, ...r.devDependencies });
|
|
156
|
+
const lines = [`Added ${names.join(", ")} from ${source.location} (Zengin UI ${index.version}).`];
|
|
157
|
+
for (const p of r.written)
|
|
158
|
+
lines.push(` wrote ${p}`);
|
|
159
|
+
for (const p of r.skipped)
|
|
160
|
+
lines.push(` kept ${p} (exists; --force overwrites)`);
|
|
161
|
+
if (Object.keys(missing).length) {
|
|
162
|
+
lines.push("", "Install the packages these components need:", ` npm install ${Object.entries(missing).map(([k, v]) => `${k}@"${v}"`).join(" ")}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push("", `Manifest: ${r.components.length} components in ${LAYOUT.definitionsDir}/components.json. Run zengin check to confirm the project is still clean.`);
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
/** `zengin tokens`: zengin/tokens*.json to src/styles/generated/tokens.css. */
|
|
168
|
+
export function runTokens(opts, cwd) {
|
|
169
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
170
|
+
const defs = join(projectDir, LAYOUT.definitionsDir);
|
|
171
|
+
const out = opts.out ? resolve(cwd, opts.out) : join(projectDir, "src", "styles", "generated", "tokens.css");
|
|
172
|
+
const r = writeTokensCss(defs, out);
|
|
173
|
+
return `Wrote ${relative(cwd, out)}: ${r.light} tokens, ${r.dark} dark overrides.`;
|
|
174
|
+
}
|
|
175
|
+
/** `zengin registry build`: the registry from a Zengin repository checkout, as static files. */
|
|
176
|
+
export function runRegistryBuild(opts, cwd) {
|
|
177
|
+
const root = opts.root ? resolve(cwd, opts.root) : findRepoRoot(cwd);
|
|
178
|
+
if (!opts.out)
|
|
179
|
+
throw new Error("zengin registry build needs --out <dir>.");
|
|
180
|
+
const out = resolve(cwd, opts.out);
|
|
181
|
+
const registry = buildRegistry({ root });
|
|
182
|
+
const written = writeRegistry(registry, out);
|
|
183
|
+
const counts = { component: 0, template: 0, theme: 0, lib: 0, definitions: 0, fonts: 0, icons: 0 };
|
|
184
|
+
for (const i of registry.items)
|
|
185
|
+
counts[i.type]++;
|
|
186
|
+
return `Wrote ${written.length} files to ${relative(cwd, out) || "."}: ${counts.component} components, ${counts.template} templates, ${counts.theme} themes, ${counts.lib + counts.definitions} shared items (Zengin UI ${registry.version}).`;
|
|
187
|
+
}
|
|
188
|
+
/** Packages an item needs that the project's package.json does not list. */
|
|
189
|
+
function missingDependencies(projectDir, wanted) {
|
|
190
|
+
const p = join(projectDir, "package.json");
|
|
191
|
+
if (!existsSync(p))
|
|
192
|
+
return wanted;
|
|
193
|
+
const pkg = JSON.parse(readFileSync(p, "utf8"));
|
|
194
|
+
const have = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
195
|
+
const missing = {};
|
|
196
|
+
for (const [k, v] of Object.entries(wanted))
|
|
197
|
+
if (!(k in have))
|
|
198
|
+
missing[k] = v;
|
|
199
|
+
if (Object.keys(missing).length) {
|
|
200
|
+
// Record them so a plain `npm install` later picks them up, and so the next add does not repeat them.
|
|
201
|
+
pkg.dependencies = Object.fromEntries(Object.entries({ ...(pkg.dependencies ?? {}), ...missing }).sort(([a], [b]) => a.localeCompare(b)));
|
|
202
|
+
writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
|
|
203
|
+
}
|
|
204
|
+
return missing;
|
|
205
|
+
}
|
|
206
|
+
/** The nearest directory above `from` that looks like the Zengin repository. */
|
|
207
|
+
function findRepoRoot(from) {
|
|
208
|
+
let dir = resolve(from);
|
|
209
|
+
for (;;) {
|
|
210
|
+
if (existsSync(join(dir, "packages", "ui", "zengin", "components.json")))
|
|
211
|
+
return dir;
|
|
212
|
+
const parent = resolve(dir, "..");
|
|
213
|
+
if (parent === dir)
|
|
214
|
+
throw new Error("Not inside a Zengin repository checkout. Pass --root <path>.");
|
|
215
|
+
dir = parent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** `zengin figma export|import|connect|plugin`: tokens to variables and back, Code Connect files, the plugin. */
|
|
219
|
+
export function runFigma(sub, args, opts, cwd) {
|
|
220
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
221
|
+
const defs = join(projectDir, LAYOUT.definitionsDir);
|
|
222
|
+
const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
223
|
+
const writeText = (rel, text) => {
|
|
224
|
+
const abs = resolve(projectDir, rel);
|
|
225
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
226
|
+
writeFileSync(abs, text);
|
|
227
|
+
return rel;
|
|
228
|
+
};
|
|
229
|
+
const tokensPath = join(defs, "tokens.json");
|
|
230
|
+
const darkPath = join(defs, "tokens.dark.json");
|
|
231
|
+
switch (sub) {
|
|
232
|
+
case "export": {
|
|
233
|
+
if (!existsSync(tokensPath))
|
|
234
|
+
throw new Error(`No ${LAYOUT.definitionsDir}/tokens.json in ${projectDir}.`);
|
|
235
|
+
const payload = toFigmaVariables(readJson(tokensPath), existsSync(darkPath) ? readJson(darkPath) : undefined, { collection: opts.collection });
|
|
236
|
+
const out = opts.out ?? "figma/variables.json";
|
|
237
|
+
writeText(out, JSON.stringify(payload, null, 2) + "\n");
|
|
238
|
+
return `Wrote ${out}: ${payload.variables.length} variables in "${payload.variableCollections[0].name}" with ${payload.variableModes.map((m) => m.name).join(" and ")} modes.\nImport it with the plugin (zengin figma plugin), or POST it to /v1/files/:key/variables on an Enterprise plan.`;
|
|
239
|
+
}
|
|
240
|
+
case "import": {
|
|
241
|
+
const file = args[0];
|
|
242
|
+
if (!file)
|
|
243
|
+
throw new Error("zengin figma import needs the exported variables JSON: zengin figma import figma/local.json [--write]");
|
|
244
|
+
if (!existsSync(tokensPath))
|
|
245
|
+
throw new Error(`No ${LAYOUT.definitionsDir}/tokens.json in ${projectDir}.`);
|
|
246
|
+
const local = readJson(resolve(cwd, file));
|
|
247
|
+
if (!local || typeof local !== "object" || !("meta" in local))
|
|
248
|
+
throw new Error(`${file} is not a Figma variables export (expected { meta: { variableCollections, variables } }).`);
|
|
249
|
+
const light = readJson(tokensPath);
|
|
250
|
+
const dark = existsSync(darkPath) ? readJson(darkPath) : undefined;
|
|
251
|
+
const report = fromFigmaVariables(local, light, dark, { collection: opts.collection });
|
|
252
|
+
const lines = [renderImportReport(report)];
|
|
253
|
+
if (opts.write) {
|
|
254
|
+
writeText(`${LAYOUT.definitionsDir}/tokens.json`, JSON.stringify(report.light, null, 2) + "\n");
|
|
255
|
+
if (dark !== undefined || report.changed.some((c) => c.mode === "dark") || report.added.some((c) => c.mode === "dark")) {
|
|
256
|
+
writeText(`${LAYOUT.definitionsDir}/tokens.dark.json`, JSON.stringify(report.dark, null, 2) + "\n");
|
|
257
|
+
}
|
|
258
|
+
lines.push("", `Wrote ${LAYOUT.definitionsDir}/tokens.json${dark !== undefined ? ` and tokens.dark.json` : ""}. Run zengin tokens to rebuild the stylesheet.`);
|
|
259
|
+
}
|
|
260
|
+
else if (report.changed.length || report.added.length) {
|
|
261
|
+
lines.push("", "Nothing written. Pass --write to update the token files.");
|
|
262
|
+
}
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
}
|
|
265
|
+
case "connect": {
|
|
266
|
+
const manifestPath = join(defs, "components.json");
|
|
267
|
+
if (!existsSync(manifestPath))
|
|
268
|
+
throw new Error(`No ${LAYOUT.definitionsDir}/components.json in ${projectDir}.`);
|
|
269
|
+
const manifests = readJson(manifestPath);
|
|
270
|
+
const urls = opts.map ? readJson(resolve(cwd, opts.map)) : {};
|
|
271
|
+
const files = codeConnectFiles(manifests, { urls, dir: opts.out ?? "src/figma" });
|
|
272
|
+
const written = Object.entries(files).map(([rel, text]) => writeText(rel, text));
|
|
273
|
+
const todo = manifests.filter((m) => !urls[m.name]).map((m) => m.name);
|
|
274
|
+
const lines = [`Wrote ${written.length} files: figma.config.json and one *.figma.tsx per component in ${opts.out ?? "src/figma"}.`];
|
|
275
|
+
if (todo.length)
|
|
276
|
+
lines.push(`${todo.length} without a Figma URL, marked TODO: ${todo.join(", ")}. Pass --map <json> with { "Button": "https://www.figma.com/design/...?node-id=..." }.`);
|
|
277
|
+
lines.push("Then: npx figma connect publish");
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
case "plugin": {
|
|
281
|
+
const dir = resolve(projectDir, opts.out ?? "figma/plugin");
|
|
282
|
+
const written = writePlugin(dir);
|
|
283
|
+
return `Wrote ${written.length} files to ${relative(cwd, dir) || "."}.\nIn Figma: Plugins, Development, Import plugin from manifest, choose manifest.json. Import pastes the payload from zengin figma export; Export produces what zengin figma import reads.`;
|
|
284
|
+
}
|
|
285
|
+
default:
|
|
286
|
+
throw new Error("zengin figma supports: export [--out file] [--collection name], import <local.json> [--write], connect [--map urls.json] [--out dir], plugin [--out dir]");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** `zengin mock <presets...> | --schema file`: typed, seeded fixture modules into src/mock. */
|
|
290
|
+
export function runMock(names, opts, cwd) {
|
|
291
|
+
const projectDir = opts.dir ? resolve(cwd, opts.dir) : cwd;
|
|
292
|
+
let schema;
|
|
293
|
+
if (opts.schema) {
|
|
294
|
+
const parsed = JSON.parse(readFileSync(resolve(cwd, opts.schema), "utf8"));
|
|
295
|
+
if (!parsed || !Array.isArray(parsed.entities))
|
|
296
|
+
throw new Error(`${opts.schema} is not a mock schema (expected { entities: [...] }).`);
|
|
297
|
+
schema = { seed: opts.seed ?? parsed.seed, entities: parsed.entities.map((e) => ({ ...e, count: opts.count ?? e.count })) };
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
if (!names.length)
|
|
301
|
+
throw new Error(`zengin mock needs preset names or --schema <file>. Presets: ${Object.keys(PRESETS).join(", ")}.`);
|
|
302
|
+
schema = schemaFromPresets(names, { seed: opts.seed, count: opts.count });
|
|
303
|
+
}
|
|
304
|
+
const files = generateMock(schema, { dir: opts.out ?? "src/mock" });
|
|
305
|
+
const written = [];
|
|
306
|
+
for (const [rel, text] of Object.entries(files)) {
|
|
307
|
+
const abs = resolve(projectDir, rel);
|
|
308
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
309
|
+
writeFileSync(abs, text);
|
|
310
|
+
written.push(rel);
|
|
311
|
+
}
|
|
312
|
+
const entities = schema.entities.map((e) => `${e.name} (${e.count ?? 20})`).join(", ");
|
|
313
|
+
return [`Wrote ${written.length} files: ${entities}, seed ${schema.seed ?? 7}.`, ...written.map((w) => ` wrote ${w}`), "", `import { ${schema.entities[0] ? pluralName(schema.entities[0].name) : "rows"} } from "@/mock/${schema.entities[0] ? pluralName(schema.entities[0].name) : "rows"}"; the same data every run.`].join("\n");
|
|
314
|
+
}
|
|
315
|
+
function pluralName(name) {
|
|
316
|
+
const lower = name.charAt(0).toLowerCase() + name.slice(1);
|
|
317
|
+
if (/[^aeiou]y$/.test(lower))
|
|
318
|
+
return `${lower.slice(0, -1)}ies`;
|
|
319
|
+
if (/(s|x|z|ch|sh)$/.test(lower))
|
|
320
|
+
return `${lower}es`;
|
|
321
|
+
return `${lower}s`;
|
|
322
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenginui/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command line for the Zengin conformance engine: the gate for pre-commit and CI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"design-system",
|
|
9
|
+
"design-tokens",
|
|
10
|
+
"lint",
|
|
11
|
+
"conformance",
|
|
12
|
+
"coding-agents",
|
|
13
|
+
"zengin"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Timurtek/zengin.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/Timurtek/zengin/tree/main/packages/cli#readme",
|
|
21
|
+
"bugs": "https://github.com/Timurtek/zengin/issues",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"zengin": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/check.js",
|
|
29
|
+
"types": "./dist/check.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@zenginui/adapter-css": "0.1.0",
|
|
39
|
+
"@zenginui/adapter-shadcn": "0.1.0",
|
|
40
|
+
"@zenginui/engine": "0.1.0",
|
|
41
|
+
"@zenginui/figma": "0.1.0",
|
|
42
|
+
"@zenginui/mock": "0.1.0",
|
|
43
|
+
"@zenginui/registry": "0.1.0",
|
|
44
|
+
"@zenginui/rollup": "0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22",
|
|
48
|
+
"typescript": "^5.9.2",
|
|
49
|
+
"vitest": "^5.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.json",
|
|
53
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
54
|
+
"test": "vitest run"
|
|
55
|
+
}
|
|
56
|
+
}
|