@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/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
+ }
@@ -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
+ }