@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { FAMILY_NOTES, RULE_DOCS, RULE_IDS } from "@zenginui/engine";
|
|
4
|
+
import { DEFAULT_CHECK, runCheck } from "./check.js";
|
|
5
|
+
import { renderGithub, renderJson, renderPretty } from "./format-cli.js";
|
|
6
|
+
import { init, initFromPackage, initFromShadcn } from "./init.js";
|
|
7
|
+
import { historyPath, renderRollup, runReport, runRollup } from "./report.js";
|
|
8
|
+
import { runAdd, runBrand, runCreate, runFigma, runFonts, runIcons, runMock, runUpgrade, runRegistryBuild, runTheme, runTokens } from "./scaffold.js";
|
|
9
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
const HELP = `zengin: design-system conformance, enforceable.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
zengin check [paths...] [options] check files (default: everything in scope)
|
|
14
|
+
zengin explain [rule] what each rule checks
|
|
15
|
+
zengin init write a zengin.config.yaml in the current directory
|
|
16
|
+
zengin init --from shadcn derive tokens, manifest and config from a shadcn/ui project
|
|
17
|
+
zengin init --from package <name> derive them from an installed package: its CSS variables and type declarations
|
|
18
|
+
zengin report [--out file] one repository's snapshot: violations plus inventory, as JSON, for the rollup
|
|
19
|
+
zengin report --into <dir> the same, filed as <dir>/<repo>/<time>.json so the directory is the history
|
|
20
|
+
zengin rollup <snapshots|dirs...> drift, adoption and trends across repositories, from report snapshots
|
|
21
|
+
zengin create <dir> a new project on Zengin UI: components copied in, engine, MCP, hook, Storybook wired
|
|
22
|
+
zengin add <items...> components or templates from the registry into this project
|
|
23
|
+
zengin upgrade [items...] what changed upstream since the components were copied; --write takes it
|
|
24
|
+
zengin theme [name] list the registry's themes, or swap this project's brand for one
|
|
25
|
+
zengin fonts [name] list the registry's font pairings, or set this project's three font tokens to one
|
|
26
|
+
zengin icons [set] list the registry's icon sets, or draw this project's icon vocabulary from one (react-icons)
|
|
27
|
+
zengin brand --name <name> a brand from a name, a logo or a color: tokens, favicon, wordmark, index.html
|
|
28
|
+
zengin tokens zengin/tokens*.json to src/styles/generated/tokens.css
|
|
29
|
+
zengin mock <presets...> typed, seeded mock data modules into src/mock (users, customers, invoices, ...)
|
|
30
|
+
zengin figma export tokens to a Figma Variables payload (figma/variables.json)
|
|
31
|
+
zengin figma import <local.json> variables exported from Figma back into the token files, with a report
|
|
32
|
+
zengin figma connect Code Connect files from zengin/components.json
|
|
33
|
+
zengin figma plugin the plugin that imports and exports variables in any Figma file
|
|
34
|
+
zengin registry build --out <dir> build the registry from a Zengin repository checkout
|
|
35
|
+
|
|
36
|
+
Init options:
|
|
37
|
+
--from shadcn read the theme CSS, Tailwind config and components/ui; write zengin/ and zengin.config.yaml
|
|
38
|
+
--from package <name> read node_modules/<name>: tokens from its stylesheet (names kept), manifest from its .d.ts
|
|
39
|
+
--dir <path> project directory (default: cwd)
|
|
40
|
+
--force overwrite existing zengin/ definitions and config
|
|
41
|
+
|
|
42
|
+
Create and add options:
|
|
43
|
+
--template <name> blank | marketing | review | saas | chat | auth | docs | storefront (create; default: blank)
|
|
44
|
+
--theme <name> apply a registry theme after the template (create)
|
|
45
|
+
--name <name> package name (create; default: the directory name)
|
|
46
|
+
--registry <dir|url> where items come from (default: $ZENGIN_REGISTRY or the public registry)
|
|
47
|
+
--no-storybook skip the Storybook config and stories (create)
|
|
48
|
+
--framework <name> vite (default) | next: the App Router under src/app, the template mounted client-side (create)
|
|
49
|
+
--local <repo> link the Zengin packages from a repository checkout instead of npm (create)
|
|
50
|
+
--dir <path> project directory (add, tokens; default: cwd)
|
|
51
|
+
--force overwrite files that already exist (add); take upstream over a conflict (upgrade)
|
|
52
|
+
--write upgrade: apply the plan (a report only, otherwise)
|
|
53
|
+
|
|
54
|
+
Brand options:
|
|
55
|
+
--name <name> the product's name (required)
|
|
56
|
+
--logo <file> SVG, PNG, JPEG or WebP; an SVG also supplies the primary color
|
|
57
|
+
--primary <hex> the primary color; wins over the logo
|
|
58
|
+
--font-display <f> Google Fonts family for headlines
|
|
59
|
+
--font-sans <f> Google Fonts family for text
|
|
60
|
+
--font-mono <f> Google Fonts family for code
|
|
61
|
+
--radius <r> sharp | soft | round (default: soft, the system's own radii)
|
|
62
|
+
--fonts <pairing> a registry pairing instead of the three --font-* families
|
|
63
|
+
|
|
64
|
+
Fonts options:
|
|
65
|
+
--self-host download the woff2 files into public/fonts and write src/theme/fonts.css; no Google Fonts at runtime
|
|
66
|
+
|
|
67
|
+
Mock options:
|
|
68
|
+
--schema <json> your own entities instead of presets
|
|
69
|
+
--count <n> rows per entity
|
|
70
|
+
--seed <n> a different draw of the same data
|
|
71
|
+
--out <dir> where the modules go (default: src/mock)
|
|
72
|
+
|
|
73
|
+
Figma options:
|
|
74
|
+
--collection <name> the variable collection (default: Zengin)
|
|
75
|
+
--write import: update zengin/tokens*.json (a report only, otherwise)
|
|
76
|
+
--map <json> connect: Figma component URLs by component name
|
|
77
|
+
--out <path> export: the payload file; connect and plugin: the directory
|
|
78
|
+
|
|
79
|
+
Registry options:
|
|
80
|
+
--root <path> the Zengin repository (default: found above cwd)
|
|
81
|
+
--out <dir> where to write index.json and items/
|
|
82
|
+
|
|
83
|
+
Report options:
|
|
84
|
+
--repo <name> repository name in the snapshot (default: from the git remote, else the directory)
|
|
85
|
+
--include-violations keep the full violation list in the snapshot (counts only by default)
|
|
86
|
+
--out <file> write the snapshot here instead of stdout
|
|
87
|
+
|
|
88
|
+
Rollup options:
|
|
89
|
+
--into <dir> report: file the snapshot under <dir>/<repo>/<time>.json; rollup reads the directory
|
|
90
|
+
--at <iso> report: the snapshot's moment, with --commit and --ref, when backfilling from an older commit
|
|
91
|
+
--previous <file> a previous rollup JSON, for deltas; with a history directory the prior run is the default
|
|
92
|
+
--format <fmt> markdown | json | html (default: markdown)
|
|
93
|
+
--out <file> write the rollup here instead of stdout
|
|
94
|
+
|
|
95
|
+
Check options:
|
|
96
|
+
--config <path> zengin.config.yaml (default: nearest one above cwd)
|
|
97
|
+
--changed [ref] only files changed since <ref> (default HEAD), plus uncommitted and untracked
|
|
98
|
+
--staged only files staged in git (pre-commit)
|
|
99
|
+
--rule <id> only this rule; repeatable. One of: ${RULE_IDS.join(", ")}
|
|
100
|
+
--severity <level> only this severity: error | warn | info
|
|
101
|
+
--fail-on <level> lowest severity that fails the run: error | warn | info | never (default: error)
|
|
102
|
+
--format <fmt> pretty | json | github (default: pretty; github emits PR annotations)
|
|
103
|
+
--max <n> violations shown in pretty output (default: 200)
|
|
104
|
+
|
|
105
|
+
Exit codes: 0 clean or below --fail-on, 1 violations at or above --fail-on, 2 usage or configuration error.
|
|
106
|
+
`;
|
|
107
|
+
export function parseArgs(argv, cwd) {
|
|
108
|
+
const check = { ...DEFAULT_CHECK, cwd, paths: [] };
|
|
109
|
+
const positional = [];
|
|
110
|
+
let command;
|
|
111
|
+
const rules = [];
|
|
112
|
+
const initOpts = { force: false };
|
|
113
|
+
const reportOpts = { includeViolations: false };
|
|
114
|
+
const rollupOpts = { format: "markdown" };
|
|
115
|
+
const scaffold = { storybook: true, force: false, list: false, write: false };
|
|
116
|
+
let rawFormat;
|
|
117
|
+
for (let i = 0; i < argv.length; i++) {
|
|
118
|
+
const a = argv[i];
|
|
119
|
+
const value = () => {
|
|
120
|
+
const v = argv[++i];
|
|
121
|
+
if (v === undefined)
|
|
122
|
+
throw new Error(`${a} needs a value`);
|
|
123
|
+
return v;
|
|
124
|
+
};
|
|
125
|
+
if (!a.startsWith("-") && !command) {
|
|
126
|
+
if (a === "check" || a === "explain" || a === "init" || a === "report" || a === "rollup" || a === "create" || a === "add" || a === "tokens" || a === "registry" || a === "theme" || a === "fonts" || a === "icons" || a === "upgrade" || a === "brand" || a === "figma" || a === "mock" || a === "help")
|
|
127
|
+
command = a;
|
|
128
|
+
else {
|
|
129
|
+
command = "check";
|
|
130
|
+
positional.push(a);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (!a.startsWith("-"))
|
|
134
|
+
positional.push(a);
|
|
135
|
+
else if (a === "-h" || a === "--help")
|
|
136
|
+
command = "help";
|
|
137
|
+
else if (a === "--config" || a === "-c")
|
|
138
|
+
check.config = value();
|
|
139
|
+
else if (a.startsWith("--config="))
|
|
140
|
+
check.config = a.slice(9);
|
|
141
|
+
else if (a === "--changed")
|
|
142
|
+
check.changed = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : "HEAD";
|
|
143
|
+
else if (a.startsWith("--changed="))
|
|
144
|
+
check.changed = a.slice(10);
|
|
145
|
+
else if (a === "--staged")
|
|
146
|
+
check.staged = true;
|
|
147
|
+
else if (a === "--rule")
|
|
148
|
+
rules.push(asRule(value()));
|
|
149
|
+
else if (a.startsWith("--rule="))
|
|
150
|
+
rules.push(asRule(a.slice(7)));
|
|
151
|
+
else if (a === "--severity")
|
|
152
|
+
check.severity = asSeverity(value());
|
|
153
|
+
else if (a.startsWith("--severity="))
|
|
154
|
+
check.severity = asSeverity(a.slice(11));
|
|
155
|
+
else if (a === "--fail-on")
|
|
156
|
+
check.failOn = asFailOn(value());
|
|
157
|
+
else if (a.startsWith("--fail-on="))
|
|
158
|
+
check.failOn = asFailOn(a.slice(10));
|
|
159
|
+
else if (a === "--format")
|
|
160
|
+
rawFormat = value();
|
|
161
|
+
else if (a.startsWith("--format="))
|
|
162
|
+
rawFormat = a.slice(9);
|
|
163
|
+
else if (a === "--max")
|
|
164
|
+
check.max = asInt(value());
|
|
165
|
+
else if (a.startsWith("--max="))
|
|
166
|
+
check.max = asInt(a.slice(6));
|
|
167
|
+
else if (a === "--from")
|
|
168
|
+
initOpts.from = value();
|
|
169
|
+
else if (a.startsWith("--from="))
|
|
170
|
+
initOpts.from = a.slice(7);
|
|
171
|
+
else if (a === "--dir")
|
|
172
|
+
initOpts.dir = scaffold.dir = value();
|
|
173
|
+
else if (a.startsWith("--dir="))
|
|
174
|
+
initOpts.dir = scaffold.dir = a.slice(6);
|
|
175
|
+
else if (a === "--force")
|
|
176
|
+
initOpts.force = scaffold.force = true;
|
|
177
|
+
else if (a === "--write")
|
|
178
|
+
scaffold.write = true;
|
|
179
|
+
else if (a === "--schema")
|
|
180
|
+
scaffold.schema = value();
|
|
181
|
+
else if (a.startsWith("--schema="))
|
|
182
|
+
scaffold.schema = a.slice(9);
|
|
183
|
+
else if (a === "--count")
|
|
184
|
+
scaffold.count = asInt(value());
|
|
185
|
+
else if (a.startsWith("--count="))
|
|
186
|
+
scaffold.count = asInt(a.slice(8));
|
|
187
|
+
else if (a === "--seed")
|
|
188
|
+
scaffold.seed = asInt(value());
|
|
189
|
+
else if (a.startsWith("--seed="))
|
|
190
|
+
scaffold.seed = asInt(a.slice(7));
|
|
191
|
+
else if (a === "--map")
|
|
192
|
+
scaffold.map = value();
|
|
193
|
+
else if (a.startsWith("--map="))
|
|
194
|
+
scaffold.map = a.slice(6);
|
|
195
|
+
else if (a === "--collection")
|
|
196
|
+
scaffold.collection = value();
|
|
197
|
+
else if (a.startsWith("--collection="))
|
|
198
|
+
scaffold.collection = a.slice(13);
|
|
199
|
+
else if (a === "--theme")
|
|
200
|
+
scaffold.theme = value();
|
|
201
|
+
else if (a.startsWith("--theme="))
|
|
202
|
+
scaffold.theme = a.slice(8);
|
|
203
|
+
else if (a === "--list")
|
|
204
|
+
scaffold.list = true;
|
|
205
|
+
else if (a === "--logo")
|
|
206
|
+
scaffold.logo = value();
|
|
207
|
+
else if (a.startsWith("--logo="))
|
|
208
|
+
scaffold.logo = a.slice(7);
|
|
209
|
+
else if (a === "--primary")
|
|
210
|
+
scaffold.primary = value();
|
|
211
|
+
else if (a.startsWith("--primary="))
|
|
212
|
+
scaffold.primary = a.slice(10);
|
|
213
|
+
else if (a === "--fonts")
|
|
214
|
+
scaffold.fonts = value();
|
|
215
|
+
else if (a.startsWith("--fonts="))
|
|
216
|
+
scaffold.fonts = a.slice(8);
|
|
217
|
+
else if (a === "--self-host")
|
|
218
|
+
scaffold.selfHost = true;
|
|
219
|
+
else if (a === "--font-display")
|
|
220
|
+
scaffold.fontDisplay = value();
|
|
221
|
+
else if (a.startsWith("--font-display="))
|
|
222
|
+
scaffold.fontDisplay = a.slice(15);
|
|
223
|
+
else if (a === "--font-sans")
|
|
224
|
+
scaffold.fontSans = value();
|
|
225
|
+
else if (a.startsWith("--font-sans="))
|
|
226
|
+
scaffold.fontSans = a.slice(12);
|
|
227
|
+
else if (a === "--font-mono")
|
|
228
|
+
scaffold.fontMono = value();
|
|
229
|
+
else if (a.startsWith("--font-mono="))
|
|
230
|
+
scaffold.fontMono = a.slice(12);
|
|
231
|
+
else if (a === "--radius")
|
|
232
|
+
scaffold.radius = asRadius(value());
|
|
233
|
+
else if (a.startsWith("--radius="))
|
|
234
|
+
scaffold.radius = asRadius(a.slice(9));
|
|
235
|
+
else if (a === "--template")
|
|
236
|
+
scaffold.template = value();
|
|
237
|
+
else if (a.startsWith("--template="))
|
|
238
|
+
scaffold.template = a.slice(11);
|
|
239
|
+
else if (a === "--name")
|
|
240
|
+
scaffold.name = value();
|
|
241
|
+
else if (a.startsWith("--name="))
|
|
242
|
+
scaffold.name = a.slice(7);
|
|
243
|
+
else if (a === "--registry")
|
|
244
|
+
scaffold.registry = value();
|
|
245
|
+
else if (a.startsWith("--registry="))
|
|
246
|
+
scaffold.registry = a.slice(11);
|
|
247
|
+
else if (a === "--no-storybook")
|
|
248
|
+
scaffold.storybook = false;
|
|
249
|
+
else if (a === "--framework")
|
|
250
|
+
scaffold.framework = asFramework(value());
|
|
251
|
+
else if (a.startsWith("--framework="))
|
|
252
|
+
scaffold.framework = asFramework(a.slice(12));
|
|
253
|
+
else if (a === "--local")
|
|
254
|
+
scaffold.local = value();
|
|
255
|
+
else if (a.startsWith("--local="))
|
|
256
|
+
scaffold.local = a.slice(8);
|
|
257
|
+
else if (a === "--root")
|
|
258
|
+
scaffold.root = value();
|
|
259
|
+
else if (a.startsWith("--root="))
|
|
260
|
+
scaffold.root = a.slice(7);
|
|
261
|
+
else if (a === "--repo")
|
|
262
|
+
reportOpts.repo = value();
|
|
263
|
+
else if (a.startsWith("--repo="))
|
|
264
|
+
reportOpts.repo = a.slice(7);
|
|
265
|
+
else if (a === "--include-violations")
|
|
266
|
+
reportOpts.includeViolations = true;
|
|
267
|
+
else if (a === "--into")
|
|
268
|
+
reportOpts.into = value();
|
|
269
|
+
else if (a.startsWith("--into="))
|
|
270
|
+
reportOpts.into = a.slice(7);
|
|
271
|
+
else if (a === "--at")
|
|
272
|
+
reportOpts.at = value();
|
|
273
|
+
else if (a.startsWith("--at="))
|
|
274
|
+
reportOpts.at = a.slice(5);
|
|
275
|
+
else if (a === "--commit")
|
|
276
|
+
reportOpts.commit = value();
|
|
277
|
+
else if (a.startsWith("--commit="))
|
|
278
|
+
reportOpts.commit = a.slice(9);
|
|
279
|
+
else if (a === "--ref")
|
|
280
|
+
reportOpts.ref = value();
|
|
281
|
+
else if (a.startsWith("--ref="))
|
|
282
|
+
reportOpts.ref = a.slice(6);
|
|
283
|
+
else if (a === "--out")
|
|
284
|
+
reportOpts.out = rollupOpts.out = scaffold.out = value();
|
|
285
|
+
else if (a.startsWith("--out="))
|
|
286
|
+
reportOpts.out = rollupOpts.out = scaffold.out = a.slice(6);
|
|
287
|
+
else if (a === "--previous")
|
|
288
|
+
rollupOpts.previous = value();
|
|
289
|
+
else if (a.startsWith("--previous="))
|
|
290
|
+
rollupOpts.previous = a.slice(11);
|
|
291
|
+
else
|
|
292
|
+
throw new Error(`Unknown option ${a}. Try zengin --help.`);
|
|
293
|
+
}
|
|
294
|
+
if (rules.length)
|
|
295
|
+
check.rules = rules;
|
|
296
|
+
check.paths = command === "check" || command === undefined ? positional : [];
|
|
297
|
+
if (rawFormat !== undefined) {
|
|
298
|
+
if (command === "rollup") {
|
|
299
|
+
if (!["markdown", "json", "html"].includes(rawFormat))
|
|
300
|
+
throw new Error(`--format must be markdown, json or html for rollup (got ${rawFormat}).`);
|
|
301
|
+
rollupOpts.format = rawFormat;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
check.format = asFormat(rawFormat);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (initOpts.from && initOpts.from !== "shadcn" && initOpts.from !== "package")
|
|
308
|
+
throw new Error(`--from supports "shadcn" and "package <name>" (got ${initOpts.from}).`);
|
|
309
|
+
return { command: command ?? "check", positional, check, init: initOpts, report: reportOpts, rollup: rollupOpts, scaffold };
|
|
310
|
+
}
|
|
311
|
+
function asRadius(s) {
|
|
312
|
+
if (!["sharp", "soft", "round"].includes(s))
|
|
313
|
+
throw new Error(`--radius must be sharp, soft or round (got ${s}).`);
|
|
314
|
+
return s;
|
|
315
|
+
}
|
|
316
|
+
function asRule(s) {
|
|
317
|
+
if (!RULE_IDS.includes(s))
|
|
318
|
+
throw new Error(`Unknown rule "${s}". Rules: ${RULE_IDS.join(", ")}.`);
|
|
319
|
+
return s;
|
|
320
|
+
}
|
|
321
|
+
function asSeverity(s) {
|
|
322
|
+
if (!["error", "warn", "info"].includes(s))
|
|
323
|
+
throw new Error(`--severity must be error, warn or info (got ${s}).`);
|
|
324
|
+
return s;
|
|
325
|
+
}
|
|
326
|
+
function asFailOn(s) {
|
|
327
|
+
if (!["error", "warn", "info", "never"].includes(s))
|
|
328
|
+
throw new Error(`--fail-on must be error, warn, info or never (got ${s}).`);
|
|
329
|
+
return s;
|
|
330
|
+
}
|
|
331
|
+
function asFormat(s) {
|
|
332
|
+
if (!["pretty", "json", "github"].includes(s))
|
|
333
|
+
throw new Error(`--format must be pretty, json or github (got ${s}).`);
|
|
334
|
+
return s;
|
|
335
|
+
}
|
|
336
|
+
function asInt(s) {
|
|
337
|
+
const n = Number(s);
|
|
338
|
+
if (!Number.isInteger(n) || n < 1)
|
|
339
|
+
throw new Error(`--max must be a positive integer (got ${s}).`);
|
|
340
|
+
return n;
|
|
341
|
+
}
|
|
342
|
+
export function explain(rule) {
|
|
343
|
+
const ids = rule ? [asRule(rule)] : RULE_IDS;
|
|
344
|
+
const lines = [];
|
|
345
|
+
for (const id of ids) {
|
|
346
|
+
const d = RULE_DOCS[id];
|
|
347
|
+
lines.push(`${id} (${d.family})`, ` ${d.description}`, "");
|
|
348
|
+
}
|
|
349
|
+
if (!rule)
|
|
350
|
+
lines.push(FAMILY_NOTES);
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
|
353
|
+
async function main() {
|
|
354
|
+
const parsed = parseArgs(process.argv.slice(2), process.cwd());
|
|
355
|
+
switch (parsed.command) {
|
|
356
|
+
case "help":
|
|
357
|
+
process.stdout.write(HELP);
|
|
358
|
+
return;
|
|
359
|
+
case "explain":
|
|
360
|
+
process.stdout.write(explain(parsed.positional[0]) + "\n");
|
|
361
|
+
return;
|
|
362
|
+
case "init": {
|
|
363
|
+
const dir = parsed.init.dir ? resolve(process.cwd(), parsed.init.dir) : process.cwd();
|
|
364
|
+
if (parsed.init.from === "shadcn") {
|
|
365
|
+
process.stdout.write(initFromShadcn(dir, parsed.init.force) + "\n");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (parsed.init.from === "package") {
|
|
369
|
+
const pkgName = parsed.positional[0];
|
|
370
|
+
if (!pkgName)
|
|
371
|
+
throw new Error("zengin init --from package needs the package name, e.g. zengin init --from package @umami/react-zen");
|
|
372
|
+
process.stdout.write(initFromPackage(dir, pkgName, parsed.init.force) + "\n");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const path = init(dir);
|
|
376
|
+
process.stdout.write(`Wrote ${path}. Edit system.package to point at your design system, then run: zengin check\n`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
case "create":
|
|
380
|
+
process.stdout.write((await runCreate(parsed.positional[0], parsed.scaffold, process.cwd())) + "\n");
|
|
381
|
+
return;
|
|
382
|
+
case "add":
|
|
383
|
+
process.stdout.write((await runAdd(parsed.positional, parsed.scaffold, process.cwd())) + "\n");
|
|
384
|
+
return;
|
|
385
|
+
case "mock":
|
|
386
|
+
process.stdout.write(runMock(parsed.positional, parsed.scaffold, process.cwd()) + "\n");
|
|
387
|
+
return;
|
|
388
|
+
case "figma":
|
|
389
|
+
process.stdout.write(runFigma(parsed.positional[0], parsed.positional.slice(1), parsed.scaffold, process.cwd()) + "\n");
|
|
390
|
+
return;
|
|
391
|
+
case "upgrade":
|
|
392
|
+
process.stdout.write((await runUpgrade(parsed.positional, parsed.scaffold, process.cwd())) + "\n");
|
|
393
|
+
return;
|
|
394
|
+
case "icons":
|
|
395
|
+
process.stdout.write((await runIcons(parsed.positional[0], parsed.scaffold, process.cwd())) + "\n");
|
|
396
|
+
return;
|
|
397
|
+
case "fonts":
|
|
398
|
+
process.stdout.write((await runFonts(parsed.positional[0], parsed.scaffold, process.cwd())) + "\n");
|
|
399
|
+
return;
|
|
400
|
+
case "theme":
|
|
401
|
+
process.stdout.write((await runTheme(parsed.positional[0], parsed.scaffold, process.cwd())) + "\n");
|
|
402
|
+
return;
|
|
403
|
+
case "brand":
|
|
404
|
+
process.stdout.write((await runBrand(parsed.scaffold, process.cwd())) + "\n");
|
|
405
|
+
return;
|
|
406
|
+
case "tokens":
|
|
407
|
+
process.stdout.write(runTokens(parsed.scaffold, process.cwd()) + "\n");
|
|
408
|
+
return;
|
|
409
|
+
case "registry": {
|
|
410
|
+
if (parsed.positional[0] !== "build")
|
|
411
|
+
throw new Error("zengin registry supports: build --out <dir>");
|
|
412
|
+
process.stdout.write(runRegistryBuild(parsed.scaffold, process.cwd()) + "\n");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
case "report": {
|
|
416
|
+
const snapshot = await runReport({ config: parsed.check.config, cwd: process.cwd(), repo: parsed.report.repo, includeViolations: parsed.report.includeViolations, at: parsed.report.at, commit: parsed.report.commit, ref: parsed.report.ref });
|
|
417
|
+
if (parsed.report.into) {
|
|
418
|
+
const target = historyPath(resolve(process.cwd(), parsed.report.into), snapshot);
|
|
419
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
420
|
+
writeFileSync(target, JSON.stringify(snapshot, null, 2) + "\n");
|
|
421
|
+
process.stdout.write(`Wrote ${relative(process.cwd(), target).replace(/\\/g, "/")}: ${snapshot.summary.total} violations, ${snapshot.inventory.suppressions} suppressions, ${Object.keys(snapshot.inventory.components).length} components in use.\n`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const text = JSON.stringify(snapshot, null, 2);
|
|
425
|
+
if (parsed.report.out) {
|
|
426
|
+
writeFileSync(resolve(process.cwd(), parsed.report.out), text + "\n");
|
|
427
|
+
process.stdout.write(`Wrote ${parsed.report.out}: ${snapshot.summary.total} violations, ${snapshot.inventory.suppressions} suppressions, ${snapshot.inventory.ownedFiles} owned files, ${Object.keys(snapshot.inventory.components).length} components in use.\n`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
process.stdout.write(text + "\n");
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
case "rollup": {
|
|
435
|
+
const result = runRollup({ cwd: process.cwd(), snapshots: parsed.positional, previous: parsed.rollup.previous });
|
|
436
|
+
const text = renderRollup(result, parsed.rollup.format);
|
|
437
|
+
if (parsed.rollup.out) {
|
|
438
|
+
mkdirSync(dirname(resolve(process.cwd(), parsed.rollup.out)), { recursive: true });
|
|
439
|
+
writeFileSync(resolve(process.cwd(), parsed.rollup.out), text + (text.endsWith("\n") ? "" : "\n"));
|
|
440
|
+
process.stdout.write(`Wrote ${parsed.rollup.out}: ${result.totals.repos} repositories, ${result.totals.violations} violations.\n`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
process.stdout.write(text + "\n");
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
case "check": {
|
|
448
|
+
const result = await runCheck(parsed.check);
|
|
449
|
+
const out = parsed.check.format === "json" ? renderJson(result) : parsed.check.format === "github" ? renderGithub(result) : renderPretty(result, parsed.check.max);
|
|
450
|
+
process.stdout.write(out + "\n");
|
|
451
|
+
process.exitCode = result.exitCode;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
main().catch((e) => {
|
|
456
|
+
process.stderr.write(`zengin: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
457
|
+
process.exitCode = 2;
|
|
458
|
+
});
|
|
459
|
+
function asFramework(v) {
|
|
460
|
+
if (v !== "vite" && v !== "next")
|
|
461
|
+
throw new Error(`--framework must be vite or next (got ${v}).`);
|
|
462
|
+
return v;
|
|
463
|
+
}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const CONFIG_TEMPLATE = "# Zengin policy for this project. The design system ships the definitions; this file says how strictly they apply.\nsystem:\n package: \"@zenginui/ui\" # the package that ships tokens.json and components.json\n # version: \"1.2.0\" # read from node_modules when omitted\n sources: [\"@zenginui/ui\"] # import sources that count as the system (globs allowed)\n # definitions: ./design-system # override the definitions directory (default: node_modules/<package>/zengin)\n\nscope:\n include: [\"src/**/*.{ts,tsx,js,jsx,css}\"]\n exclude: [\"**/*.stories.{ts,tsx}\", \"**/*.test.{ts,tsx}\"]\n foundations: [\"src/theme/**\"] # literals live here; not checked\n ownership: [] # paths where you have taken ownership of a component; contract and substitution rules are off there\n\nclasses:\n tailwind: auto # auto | true | false; auto enables the adapter when package.json depends on tailwindcss\n\nrules:\n color-literal: { severity: error, allow: semantic } # allow: semantic | palette\n spacing-literal: error\n token-reference: error\n unknown-prop: error\n unknown-prop-value: error\n classname-policy: error\n component-substitution: error\n # component-substitution:\n # severity: error\n # map: { \"@headlessui/react#Dialog\": Dialog } # extra shadowed imports beyond the manifest's\n";
|
|
2
|
+
/** Writes zengin.config.yaml into `dir`. Refuses to overwrite. Returns the path written. */
|
|
3
|
+
export declare function init(dir: string): string;
|
|
4
|
+
/** Derives definitions and a config from a shadcn/ui project and writes them. Returns the report text. */
|
|
5
|
+
export declare function initFromShadcn(dir: string, force: boolean): string;
|
|
6
|
+
/** Derives definitions and a config from an installed package's stylesheet and type declarations. Returns the report text. */
|
|
7
|
+
export declare function initFromPackage(dir: string, pkgName: string, force: boolean): string;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { derivePackage, renderReport as renderPackageReport, writePackage } from "@zenginui/adapter-css";
|
|
4
|
+
import { deriveShadcn, renderReport, writeShadcn } from "@zenginui/adapter-shadcn";
|
|
5
|
+
export const CONFIG_TEMPLATE = `# Zengin policy for this project. The design system ships the definitions; this file says how strictly they apply.
|
|
6
|
+
system:
|
|
7
|
+
package: "@zenginui/ui" # the package that ships tokens.json and components.json
|
|
8
|
+
# version: "1.2.0" # read from node_modules when omitted
|
|
9
|
+
sources: ["@zenginui/ui"] # import sources that count as the system (globs allowed)
|
|
10
|
+
# definitions: ./design-system # override the definitions directory (default: node_modules/<package>/zengin)
|
|
11
|
+
|
|
12
|
+
scope:
|
|
13
|
+
include: ["src/**/*.{ts,tsx,js,jsx,css}"]
|
|
14
|
+
exclude: ["**/*.stories.{ts,tsx}", "**/*.test.{ts,tsx}"]
|
|
15
|
+
foundations: ["src/theme/**"] # literals live here; not checked
|
|
16
|
+
ownership: [] # paths where you have taken ownership of a component; contract and substitution rules are off there
|
|
17
|
+
|
|
18
|
+
classes:
|
|
19
|
+
tailwind: auto # auto | true | false; auto enables the adapter when package.json depends on tailwindcss
|
|
20
|
+
|
|
21
|
+
rules:
|
|
22
|
+
color-literal: { severity: error, allow: semantic } # allow: semantic | palette
|
|
23
|
+
spacing-literal: error
|
|
24
|
+
token-reference: error
|
|
25
|
+
unknown-prop: error
|
|
26
|
+
unknown-prop-value: error
|
|
27
|
+
classname-policy: error
|
|
28
|
+
component-substitution: error
|
|
29
|
+
# component-substitution:
|
|
30
|
+
# severity: error
|
|
31
|
+
# map: { "@headlessui/react#Dialog": Dialog } # extra shadowed imports beyond the manifest's
|
|
32
|
+
`;
|
|
33
|
+
/** Writes zengin.config.yaml into `dir`. Refuses to overwrite. Returns the path written. */
|
|
34
|
+
export function init(dir) {
|
|
35
|
+
const path = join(dir, "zengin.config.yaml");
|
|
36
|
+
if (existsSync(path))
|
|
37
|
+
throw new Error(`${path} already exists. Delete it first if you want a fresh template.`);
|
|
38
|
+
writeFileSync(path, CONFIG_TEMPLATE);
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
/** Derives definitions and a config from a shadcn/ui project and writes them. Returns the report text. */
|
|
42
|
+
export function initFromShadcn(dir, force) {
|
|
43
|
+
const derivation = deriveShadcn(dir);
|
|
44
|
+
const { written } = writeShadcn(dir, derivation, force);
|
|
45
|
+
return `Wrote ${written.join(", ")}
|
|
46
|
+
|
|
47
|
+
${renderReport(derivation)}`;
|
|
48
|
+
}
|
|
49
|
+
/** Derives definitions and a config from an installed package's stylesheet and type declarations. Returns the report text. */
|
|
50
|
+
export function initFromPackage(dir, pkgName, force) {
|
|
51
|
+
const derivation = derivePackage(dir, pkgName);
|
|
52
|
+
const { written } = writePackage(dir, derivation, force);
|
|
53
|
+
return `Wrote ${written.join(", ")}
|
|
54
|
+
|
|
55
|
+
${renderPackageReport(derivation)}`;
|
|
56
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReportSnapshot, type RollupResult } from "@zenginui/rollup";
|
|
2
|
+
export interface ReportOptions {
|
|
3
|
+
config?: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
/** Repository name for the snapshot. Default: the git remote's repo name, else the directory name. */
|
|
6
|
+
repo?: string;
|
|
7
|
+
includeViolations: boolean;
|
|
8
|
+
/** The snapshot's moment, for backfilling history from older commits. Default: now. */
|
|
9
|
+
at?: string;
|
|
10
|
+
/** Commit and ref to record instead of git's answer, for backfills. */
|
|
11
|
+
commit?: string;
|
|
12
|
+
ref?: string;
|
|
13
|
+
}
|
|
14
|
+
/** One repository, one point in time: the check plus the inventory, as JSON for the rollup. */
|
|
15
|
+
export declare function runReport(opts: ReportOptions): Promise<ReportSnapshot>;
|
|
16
|
+
/** `reports/acme-checkout/2026-09-12T06-00-00Z.json`: one file per run, named so a directory listing is a timeline. */
|
|
17
|
+
export declare function historyPath(into: string, snapshot: ReportSnapshot): string;
|
|
18
|
+
/** Name, ref and commit from git when available. Never fails: a snapshot without git metadata is still a snapshot. */
|
|
19
|
+
export declare function repoInfo(dir: string, nameOverride?: string): ReportSnapshot["repo"];
|
|
20
|
+
export interface RollupOptions {
|
|
21
|
+
cwd: string;
|
|
22
|
+
/** Snapshot files written by `zengin report`. */
|
|
23
|
+
snapshots: string[];
|
|
24
|
+
/** A previous rollup JSON to compute deltas against. */
|
|
25
|
+
previous?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function runRollup(opts: RollupOptions): RollupResult;
|
|
28
|
+
export declare function renderRollup(r: RollupResult, format: "markdown" | "json" | "html"): string;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { createEngine, loadConfigFile, readProjectFiles, resolveConfig } from "@zenginui/engine";
|
|
5
|
+
import { aggregate, buildSnapshot, isRollup, isSnapshot, renderHtml, renderMarkdown } from "@zenginui/rollup";
|
|
6
|
+
import { findConfig } from "./check.js";
|
|
7
|
+
/** One repository, one point in time: the check plus the inventory, as JSON for the rollup. */
|
|
8
|
+
export async function runReport(opts) {
|
|
9
|
+
const configPath = findConfig(opts.config, opts.cwd);
|
|
10
|
+
const { config, dir } = loadConfigFile(configPath);
|
|
11
|
+
const resolved = resolveConfig(config, dir);
|
|
12
|
+
const engine = await createEngine(resolved);
|
|
13
|
+
const files = readProjectFiles(dir, resolved.scope.include, resolved.scope.exclude);
|
|
14
|
+
const repo = repoInfo(dir, opts.repo);
|
|
15
|
+
if (opts.commit)
|
|
16
|
+
repo.commit = opts.commit;
|
|
17
|
+
if (opts.ref)
|
|
18
|
+
repo.ref = opts.ref;
|
|
19
|
+
return buildSnapshot({ engine, files, repo, includeViolations: opts.includeViolations, ...(opts.at ? { now: new Date(opts.at) } : {}) });
|
|
20
|
+
}
|
|
21
|
+
/** `reports/acme-checkout/2026-09-12T06-00-00Z.json`: one file per run, named so a directory listing is a timeline. */
|
|
22
|
+
export function historyPath(into, snapshot) {
|
|
23
|
+
const repo = snapshot.repo.name.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
24
|
+
const stamp = snapshot.generatedAt.replace(/:/g, "-").replace(/\.\d+Z$/, "Z");
|
|
25
|
+
return join(into, repo, `${stamp}.json`);
|
|
26
|
+
}
|
|
27
|
+
/** Name, ref and commit from git when available. Never fails: a snapshot without git metadata is still a snapshot. */
|
|
28
|
+
export function repoInfo(dir, nameOverride) {
|
|
29
|
+
const git = (args) => {
|
|
30
|
+
try {
|
|
31
|
+
return execFileSync("git", args, { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const remote = git(["remote", "get-url", "origin"]);
|
|
38
|
+
const fromRemote = remote ? /([^/:]+\/[^/]+?)(?:\.git)?$/.exec(remote)?.[1] : undefined;
|
|
39
|
+
const name = nameOverride ?? fromRemote ?? basename(dir);
|
|
40
|
+
const commit = git(["rev-parse", "HEAD"]);
|
|
41
|
+
const ref = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
42
|
+
return { name, ...(ref && ref !== "HEAD" ? { ref } : {}), ...(commit ? { commit } : {}) };
|
|
43
|
+
}
|
|
44
|
+
export function runRollup(opts) {
|
|
45
|
+
if (opts.snapshots.length === 0)
|
|
46
|
+
throw new Error("zengin rollup needs at least one snapshot file written by `zengin report`, or a directory of them.");
|
|
47
|
+
const files = opts.snapshots.flatMap((p) => {
|
|
48
|
+
const abs = resolve(opts.cwd, p);
|
|
49
|
+
return statSync(abs).isDirectory() ? walkJson(abs) : [abs];
|
|
50
|
+
});
|
|
51
|
+
if (files.length === 0)
|
|
52
|
+
throw new Error(`No snapshot files under ${opts.snapshots.join(", ")}.`);
|
|
53
|
+
const snapshots = files.flatMap((abs) => {
|
|
54
|
+
let json;
|
|
55
|
+
try {
|
|
56
|
+
json = JSON.parse(readFileSync(abs, "utf8"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
json = undefined;
|
|
60
|
+
}
|
|
61
|
+
if (isSnapshot(json))
|
|
62
|
+
return [json];
|
|
63
|
+
// A directory may hold the rollup's own outputs beside the snapshots; a file given by name must be one.
|
|
64
|
+
if (opts.snapshots.some((p) => resolve(opts.cwd, p) === abs))
|
|
65
|
+
throw new Error(`${abs} is not a zengin report snapshot (expected schema zengin-report/1).`);
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
if (snapshots.length === 0)
|
|
69
|
+
throw new Error(`No zengin report snapshots found under ${opts.snapshots.join(", ")}.`);
|
|
70
|
+
let previous;
|
|
71
|
+
if (opts.previous) {
|
|
72
|
+
const json = JSON.parse(readFileSync(resolve(opts.cwd, opts.previous), "utf8"));
|
|
73
|
+
if (!isRollup(json))
|
|
74
|
+
throw new Error(`${opts.previous} is not a zengin rollup (expected schema zengin-rollup/1).`);
|
|
75
|
+
previous = json;
|
|
76
|
+
}
|
|
77
|
+
return aggregate(snapshots, { previous });
|
|
78
|
+
}
|
|
79
|
+
/** Every .json file under a directory, sorted, so a directory of runs reads in order. */
|
|
80
|
+
function walkJson(dir) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const name of readdirSync(dir).sort()) {
|
|
83
|
+
const p = join(dir, name);
|
|
84
|
+
if (statSync(p).isDirectory())
|
|
85
|
+
out.push(...walkJson(p));
|
|
86
|
+
else if (name.endsWith(".json"))
|
|
87
|
+
out.push(p);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
export function renderRollup(r, format) {
|
|
92
|
+
if (format === "json")
|
|
93
|
+
return JSON.stringify(r, null, 2);
|
|
94
|
+
if (format === "html")
|
|
95
|
+
return renderHtml(r);
|
|
96
|
+
return renderMarkdown(r);
|
|
97
|
+
}
|