@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/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @zenginui/cli
|
|
2
|
+
|
|
3
|
+
The `zengin` command. The gate that actually protects the codebase: run it pre-commit and on every pull request. Same engine, same violations, same fixes as the MCP server and the hook.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
zengin check [paths...] check files (default: everything in scope)
|
|
7
|
+
zengin explain [rule] what each rule checks
|
|
8
|
+
zengin init write a zengin.config.yaml in the current directory
|
|
9
|
+
zengin create <dir> a new project that owns its components, with the engine, MCP, hook and Storybook wired
|
|
10
|
+
zengin add <items...> components or templates from the registry into this project
|
|
11
|
+
zengin theme [name] list the registry's themes, or swap this project's brand for one
|
|
12
|
+
zengin brand --name <name> a brand from a name, a logo or a color: tokens, favicon, wordmark, index.html
|
|
13
|
+
zengin tokens zengin/tokens*.json to src/styles/generated/tokens.css
|
|
14
|
+
zengin figma export|import|connect|plugin tokens to Figma variables and back, Code Connect, the plugin
|
|
15
|
+
zengin mock <presets...> typed, seeded mock data modules into src/mock
|
|
16
|
+
zengin registry build the registry, from a Zengin repository checkout
|
|
17
|
+
zengin report, rollup drift and adoption, per repository and across them
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## check
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
zengin check # whole scope, pretty output
|
|
24
|
+
zengin check src/features/review # a directory
|
|
25
|
+
zengin check --changed origin/main # only files changed since a ref, plus uncommitted and untracked
|
|
26
|
+
zengin check --staged # only staged files (pre-commit)
|
|
27
|
+
zengin check --format github # GitHub Actions annotations on the PR
|
|
28
|
+
zengin check --format json > report.json # the full violation shape, for the rollup
|
|
29
|
+
zengin check --rule color-literal --severity error
|
|
30
|
+
zengin check --fail-on never # report, never fail
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Option | Values | Default |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `--config <path>` | | nearest `zengin.config.yaml` above cwd |
|
|
36
|
+
| `--changed [ref]` | git ref | `HEAD` when given without a value |
|
|
37
|
+
| `--staged` | | off |
|
|
38
|
+
| `--rule <id>` | repeatable | all rules |
|
|
39
|
+
| `--severity` | `error`, `warn`, `info` | all |
|
|
40
|
+
| `--fail-on` | `error`, `warn`, `info`, `never` | `error` |
|
|
41
|
+
| `--format` | `pretty`, `json`, `github` | `pretty` |
|
|
42
|
+
| `--max <n>` | | `200` |
|
|
43
|
+
|
|
44
|
+
Exit codes: `0` clean or below `--fail-on`, `1` violations at or above `--fail-on`, `2` usage or configuration error.
|
|
45
|
+
|
|
46
|
+
Stylesheets outside the selected files still resolve class names for the files inside it, so `zengin check src/one/file.tsx` sees what `className="btn"` does even though the CSS file was not selected.
|
|
47
|
+
|
|
48
|
+
## Pre-commit
|
|
49
|
+
|
|
50
|
+
With [husky](https://typicode.github.io/husky/) or any pre-commit runner:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
zengin check --staged
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## GitHub Actions
|
|
57
|
+
|
|
58
|
+
Until the packages are published, build them from source in the workflow. With published packages this collapses to one `npx zenginui check` step.
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
name: design-system
|
|
62
|
+
on: [pull_request]
|
|
63
|
+
jobs:
|
|
64
|
+
zengin:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
with: { fetch-depth: 0 } # --changed needs the base ref
|
|
69
|
+
- uses: pnpm/action-setup@v4
|
|
70
|
+
- uses: actions/setup-node@v4
|
|
71
|
+
with: { node-version: 22, cache: pnpm }
|
|
72
|
+
- run: pnpm install --frozen-lockfile
|
|
73
|
+
- run: pnpm build
|
|
74
|
+
- run: pnpm exec zengin check --changed origin/${{ github.base_ref }} --format github
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`--format github` emits workflow commands, so each violation appears as an inline annotation on the changed line with the fix in the message. The job fails on `error` severity unless `--fail-on` says otherwise.
|
|
78
|
+
|
|
79
|
+
## create and add
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
zengin create acme --template marketing # blank | marketing | review | saas | chat | auth | docs | storefront
|
|
83
|
+
zengin create acme --template saas --framework next # the App Router under src/app; the template mounted client-side from page.tsx
|
|
84
|
+
zengin create acme --no-storybook
|
|
85
|
+
zengin create acme --registry ./r --local ../zengin # a local registry, packages linked from a checkout
|
|
86
|
+
cd acme && zengin add dialog tooltip # more items; --force overwrites files that exist
|
|
87
|
+
zengin tokens # after editing zengin/tokens*.json (dev and build run it)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`create` writes the project, copies the template's components in with the owned pragma, merges their manifest entries into `zengin/components.json`, builds `tokens.css`, and runs the engine on the result before it prints. `add` does the same for further items and records any npm packages they need in `package.json`. See [@zenginui/registry](../registry) for the layout and the registry format.
|
|
91
|
+
|
|
92
|
+
## upgrade
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
zengin upgrade # a report: what the registry changed since each owned file was copied, and what you changed
|
|
96
|
+
zengin upgrade --write # take every upstream change the project did not touch; move the pinned version
|
|
97
|
+
zengin upgrade button card # only these items
|
|
98
|
+
zengin upgrade --write --force # take upstream over a conflict too
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Every owned file carries the hash of what was copied in its pragma (`/* zengin-owned Button, forked from @zenginui/ui@0.1.0, sha 3f9a1c0b2d4e */`). Comparing that hash with the file now says whether you edited it; comparing it with the registry says whether the system moved. Four answers per file: `current`, `upstream` (taken with `--write`), `local` (yours, left alone), `conflict` (both moved: the report shows the diff, `--force` takes upstream). Files copied before hashes existed show as `unknown` when they differ. When nothing is left behind, `zengin.config.yaml` is moved to the registry's version.
|
|
102
|
+
|
|
103
|
+
## theme and brand
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
zengin theme # list: brutal, default, meadow, plex, spec-sheet, zengin
|
|
107
|
+
zengin theme plex # swap the brand file and the fonts link; nothing else changes
|
|
108
|
+
zengin fonts # list: archivo, brutal, dm, fraunces, geist, inter, manrope, playfair, plex, space
|
|
109
|
+
zengin fonts fraunces # the three font tokens and the fonts link; palette untouched
|
|
110
|
+
zengin fonts geist --self-host # woff2 files into public/fonts, @font-face in src/theme/fonts.css
|
|
111
|
+
zengin brand --name Acme --fonts plex # a pairing instead of --font-display/--font-sans/--font-mono
|
|
112
|
+
zengin icons # list: lucide, tabler, phosphor, heroicons, feather, radix, material, bootstrap
|
|
113
|
+
zengin icons tabler # every <Icon.Name /> draws from Tabler; direct icon-package imports become violations
|
|
114
|
+
zengin create acme --theme spec-sheet # or at creation
|
|
115
|
+
|
|
116
|
+
zengin brand --name "Acme Reviews" --logo logo.svg --font-display Archivo --font-sans Inter --radius round
|
|
117
|
+
zengin brand --name Nova --primary "#7C3AED" # from a color alone
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`theme` replaces `src/theme/brand.css` with the theme's file and puts its Google Fonts link in `index.html` (one link, marked `data-zengin="fonts"`, replaced by the next theme). `brand` derives a whole palette from one color, in OKLCH so steps look even, and pushes every pairing the components rely on (text on surface, on-primary on primary, soft-foreground on soft, and so on) until it meets WCAG AA in both schemes. It writes the brand file, `zengin/brand.json` (the inputs, for re-running), the logo into `public/`, a favicon when there is no SVG logo, `src/brand.ts` and a `BrandMark` component, and patches the title, theme-color, icon and fonts in `index.html`. An SVG logo also supplies the primary; a PNG needs `--primary`. Both commands run the engine afterwards; the project stays clean because the brand file is a foundation.
|
|
121
|
+
|
|
122
|
+
## mock
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
zengin mock customers invoices # presets: users, customers, companies, products, orders, invoices, events, messages, metrics
|
|
126
|
+
zengin mock users --count 50 --seed 3
|
|
127
|
+
zengin mock --schema mock.json # your own entities; see @zenginui/mock for the field kinds
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Writes `src/mock/rng.ts` (a seeded generator and the pools, no dependency) and one typed module per entity exporting the type, a factory and the array. The same seed gives the same data on every run, so screenshots and previews do not drift. See [@zenginui/mock](../mock).
|
|
131
|
+
|
|
132
|
+
## figma
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
zengin figma export # zengin/tokens*.json -> figma/variables.json (the Variables payload)
|
|
136
|
+
zengin figma plugin # a plugin into figma/plugin/ that imports that payload into any file
|
|
137
|
+
zengin figma import figma/local.json # what the plugin exported -> a report of what changed in Figma
|
|
138
|
+
zengin figma import figma/local.json --write # and update the token files; then zengin tokens
|
|
139
|
+
zengin figma connect --map figma/map.json # Code Connect files from zengin/components.json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
One naming rule carries both directions: `color.primary.soft` is `color/primary/soft` in Figma with `var(--color-primary-soft)` as its code syntax. See [@zenginui/figma](../figma).
|
|
143
|
+
|
|
144
|
+
## report and rollup
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
zengin report --out zengin-report.json # in each consuming repository, in CI
|
|
148
|
+
zengin rollup reports/*.json --previous last.json # across repositories; markdown by default
|
|
149
|
+
zengin rollup reports/*.json --format html --out rollup.html
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`report` is the check plus an inventory: component usage (adoption), suppressions with and without reasons, owned forks with their versions, uncontracted components, and the pinned system version. `rollup` ranks repositories by drift, shows violations per 100 files so sizes compare, flags who is behind the latest version, computes deltas against a previous rollup, and lists what needs attention in priority order. See [`@zenginui/rollup`](../rollup) for the workflow recipe.
|
|
153
|
+
|
|
154
|
+
## explain
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
zengin explain # all rules, plus how scope and suppressions work
|
|
158
|
+
zengin explain classname-policy
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## init
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
zengin init # a commented zengin.config.yaml template
|
|
165
|
+
zengin init --from shadcn # derive zengin/tokens.json, zengin/components.json and a config from a shadcn/ui project
|
|
166
|
+
zengin init --from package @umami/react-zen # the same from an installed design-system package: its CSS variables (names kept) and .d.ts
|
|
167
|
+
zengin init --from shadcn --dir ../app --force
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The template refuses to overwrite an existing config. The shadcn path reads the theme CSS, the Tailwind config and `components/ui`, writes the definitions, and prints the defaults worth reviewing. See [`@zenginui/adapter-shadcn`](../adapter-shadcn). The package path reads `node_modules/<name>`: its theme stylesheet, its precompiled utilities and its `.d.ts`; see [`@zenginui/adapter-css`](../adapter-css).
|
|
171
|
+
|
|
172
|
+
## Not included, on purpose
|
|
173
|
+
|
|
174
|
+
`zengin fix` would apply `exact` fixes automatically. It is a small addition, and it is left out for now because the first milestone's non-goals exclude tools that edit code. It is a candidate for later once the false-positive rate of the rules is measured on a real codebase.
|
|
175
|
+
|
|
176
|
+
## History
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
zengin report --into reports # files reports/<repo>/<time>.json instead of one --out file
|
|
180
|
+
zengin rollup reports --format html --out public/rollup/index.html
|
|
181
|
+
zengin report --into reports --at 2026-09-01T06:00:00Z --commit abc1234 --ref main # backfill from an older checkout
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`zengin rollup` accepts directories and reads every snapshot under them as history: the newest run per repository is the row, the one before is the delta, the series is the trend. See [`@zenginui/rollup`](../rollup#history-and-trends).
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type RuleId, type Severity, type Summary, type Violation } from "@zenginui/engine";
|
|
2
|
+
export type Format = "pretty" | "json" | "github";
|
|
3
|
+
export interface CheckOptions {
|
|
4
|
+
/** Explicit config path. Otherwise the nearest zengin.config.yaml walking up from cwd. */
|
|
5
|
+
config?: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** Project-relative or absolute paths. Empty means the whole scope. */
|
|
8
|
+
paths: string[];
|
|
9
|
+
/** Only files changed relative to this git ref. */
|
|
10
|
+
changed?: string;
|
|
11
|
+
/** Only files staged in git. */
|
|
12
|
+
staged: boolean;
|
|
13
|
+
rules?: RuleId[];
|
|
14
|
+
severity?: Severity;
|
|
15
|
+
/** Lowest severity that makes the exit code non-zero. */
|
|
16
|
+
failOn: Severity | "never";
|
|
17
|
+
format: Format;
|
|
18
|
+
/** Maximum violations rendered in pretty output. */
|
|
19
|
+
max: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_CHECK: Omit<CheckOptions, "cwd">;
|
|
22
|
+
export interface CheckResult {
|
|
23
|
+
projectDir: string;
|
|
24
|
+
configPath: string;
|
|
25
|
+
system: {
|
|
26
|
+
package: string;
|
|
27
|
+
version: string;
|
|
28
|
+
};
|
|
29
|
+
filesChecked: number;
|
|
30
|
+
violations: Violation[];
|
|
31
|
+
summary: Summary;
|
|
32
|
+
/** 0 clean or below fail-on, 1 violations at or above fail-on. Usage and config errors throw. */
|
|
33
|
+
exitCode: 0 | 1;
|
|
34
|
+
}
|
|
35
|
+
export declare function findConfig(explicit: string | undefined, startDir: string): string;
|
|
36
|
+
export declare function runCheck(opts: CheckOptions): Promise<CheckResult>;
|
|
37
|
+
/** Reads a file for callers that want to show source context. */
|
|
38
|
+
export declare function readSource(projectDir: string, rel: string): string | undefined;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { createEngine, loadConfigFile, readProjectFiles, resolveConfig, summarize, } from "@zenginui/engine";
|
|
4
|
+
import { changedFiles, stagedFiles } from "./git.js";
|
|
5
|
+
export const DEFAULT_CHECK = { paths: [], staged: false, failOn: "error", format: "pretty", max: 200 };
|
|
6
|
+
const CONFIG_NAMES = ["zengin.config.yaml", "zengin.config.yml"];
|
|
7
|
+
const RANK = { error: 3, warn: 2, info: 1 };
|
|
8
|
+
export function findConfig(explicit, startDir) {
|
|
9
|
+
if (explicit) {
|
|
10
|
+
const p = resolve(startDir, explicit);
|
|
11
|
+
if (!existsSync(p))
|
|
12
|
+
throw new Error(`Config not found at ${p}.`);
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
let dir = resolve(startDir);
|
|
16
|
+
for (;;) {
|
|
17
|
+
for (const name of CONFIG_NAMES) {
|
|
18
|
+
const p = join(dir, name);
|
|
19
|
+
if (existsSync(p))
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
22
|
+
const parent = dirname(dir);
|
|
23
|
+
if (parent === dir)
|
|
24
|
+
break;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`No zengin.config.yaml found in ${startDir} or any parent. Run \`zengin init\` to create one, or pass --config.`);
|
|
28
|
+
}
|
|
29
|
+
function selectFiles(opts, projectDir, config) {
|
|
30
|
+
const inScope = readProjectFiles(projectDir, config.scope.include, config.scope.exclude);
|
|
31
|
+
const byPath = new Map(inScope.map((f) => [f.path, f]));
|
|
32
|
+
const toRel = (p) => relative(projectDir, isAbsolute(p) ? p : resolve(opts.cwd, p)).replace(/\\/g, "/");
|
|
33
|
+
let wanted;
|
|
34
|
+
if (opts.staged)
|
|
35
|
+
wanted = stagedFiles(projectDir);
|
|
36
|
+
else if (opts.changed !== undefined)
|
|
37
|
+
wanted = changedFiles(projectDir, opts.changed);
|
|
38
|
+
if (wanted) {
|
|
39
|
+
const explicit = opts.paths.map(toRel);
|
|
40
|
+
return wanted
|
|
41
|
+
.filter((p) => byPath.has(p) && (explicit.length === 0 || explicit.some((e) => p === e || p.startsWith(e + "/"))))
|
|
42
|
+
.map((p) => byPath.get(p));
|
|
43
|
+
}
|
|
44
|
+
if (opts.paths.length === 0)
|
|
45
|
+
return inScope;
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const raw of opts.paths) {
|
|
48
|
+
const rel = toRel(raw);
|
|
49
|
+
const abs = join(projectDir, rel);
|
|
50
|
+
if (!existsSync(abs))
|
|
51
|
+
throw new Error(`Path not found: ${raw}`);
|
|
52
|
+
const hit = byPath.get(rel);
|
|
53
|
+
if (hit) {
|
|
54
|
+
out.push(hit);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// A directory: every in-scope file under it. A file outside scope: nothing, by design.
|
|
58
|
+
for (const f of inScope)
|
|
59
|
+
if (f.path.startsWith(rel + "/"))
|
|
60
|
+
out.push(f);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
export async function runCheck(opts) {
|
|
65
|
+
const configPath = findConfig(opts.config, opts.cwd);
|
|
66
|
+
const { config, dir } = loadConfigFile(configPath);
|
|
67
|
+
const resolved = resolveConfig(config, dir);
|
|
68
|
+
const engine = await createEngine(resolved);
|
|
69
|
+
const files = selectFiles(opts, dir, resolved);
|
|
70
|
+
// Stylesheets outside the selection still resolve class names for the files in it.
|
|
71
|
+
engine.loadStylesheets(readProjectFiles(dir, resolved.scope.include, resolved.scope.exclude).filter((f) => /\.css$/.test(f.path)));
|
|
72
|
+
let violations = engine.check(files);
|
|
73
|
+
if (opts.rules)
|
|
74
|
+
violations = violations.filter((v) => opts.rules.includes(v.rule));
|
|
75
|
+
if (opts.severity)
|
|
76
|
+
violations = violations.filter((v) => v.severity === opts.severity);
|
|
77
|
+
const failing = opts.failOn !== "never" && violations.some((v) => RANK[v.severity] >= RANK[opts.failOn]);
|
|
78
|
+
return {
|
|
79
|
+
projectDir: dir,
|
|
80
|
+
configPath,
|
|
81
|
+
system: { package: resolved.system.package, version: resolved.system.version },
|
|
82
|
+
filesChecked: files.length,
|
|
83
|
+
violations,
|
|
84
|
+
summary: summarize(violations),
|
|
85
|
+
exitCode: failing ? 1 : 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Reads a file for callers that want to show source context. */
|
|
89
|
+
export function readSource(projectDir, rel) {
|
|
90
|
+
const p = join(projectDir, rel);
|
|
91
|
+
return existsSync(p) ? readFileSync(p, "utf8") : undefined;
|
|
92
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CheckResult } from "./check.js";
|
|
2
|
+
/** Terminal rendering: grouped by file, one line per violation, fix underneath. */
|
|
3
|
+
export declare function renderPretty(result: CheckResult, max: number): string;
|
|
4
|
+
/** GitHub Actions workflow commands, so violations become inline annotations on the pull request. */
|
|
5
|
+
export declare function renderGithub(result: CheckResult): string;
|
|
6
|
+
export declare function renderJson(result: CheckResult): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { renderSummary } from "@zenginui/engine";
|
|
2
|
+
const useColor = () => process.stdout.isTTY === true && !process.env["NO_COLOR"] && process.env["TERM"] !== "dumb";
|
|
3
|
+
function paint(code) {
|
|
4
|
+
return useColor() ? (s) => `[${code}m${s}[0m` : (s) => s;
|
|
5
|
+
}
|
|
6
|
+
const bold = paint("1");
|
|
7
|
+
const dim = paint("2");
|
|
8
|
+
const red = paint("31");
|
|
9
|
+
const yellow = paint("33");
|
|
10
|
+
const blue = paint("34");
|
|
11
|
+
const green = paint("32");
|
|
12
|
+
const cyan = paint("36");
|
|
13
|
+
const SEVERITY_PAINT = { error: red, warn: yellow, info: blue };
|
|
14
|
+
/** Terminal rendering: grouped by file, one line per violation, fix underneath. */
|
|
15
|
+
export function renderPretty(result, max) {
|
|
16
|
+
const { violations, filesChecked, system } = result;
|
|
17
|
+
const lines = [];
|
|
18
|
+
const shown = violations.slice(0, max);
|
|
19
|
+
let current = "";
|
|
20
|
+
for (const v of shown) {
|
|
21
|
+
if (v.file !== current) {
|
|
22
|
+
current = v.file;
|
|
23
|
+
if (lines.length)
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(bold(v.file));
|
|
26
|
+
}
|
|
27
|
+
const loc = dim(`${v.range.start.line}:${v.range.start.col}`);
|
|
28
|
+
lines.push(` ${loc} ${SEVERITY_PAINT[v.severity](v.severity.padEnd(5))} ${cyan(v.rule)} ${v.message}`);
|
|
29
|
+
lines.push(` ${dim("found")} ${oneLine(v.found)}`);
|
|
30
|
+
if (v.fix.replace !== null)
|
|
31
|
+
lines.push(` ${dim(`fix (${v.fix.confidence})`)} ${green(oneLine(v.fix.replace))}`);
|
|
32
|
+
if (v.fix.note)
|
|
33
|
+
lines.push(` ${dim("note")} ${v.fix.note}`);
|
|
34
|
+
if (v.note)
|
|
35
|
+
lines.push(` ${dim("note")} ${v.note}`);
|
|
36
|
+
}
|
|
37
|
+
if (lines.length)
|
|
38
|
+
lines.push("");
|
|
39
|
+
const summary = renderSummary(result.summary);
|
|
40
|
+
const tail = `${filesChecked} file${filesChecked === 1 ? "" : "s"} checked against ${system.package}@${system.version}: ${summary}`;
|
|
41
|
+
lines.push(violations.length ? (result.exitCode ? red(tail) : yellow(tail)) : green(tail));
|
|
42
|
+
if (shown.length < violations.length)
|
|
43
|
+
lines.push(dim(`Showing the first ${shown.length}. Raise --max or use --format json for all.`));
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
/** GitHub Actions workflow commands, so violations become inline annotations on the pull request. */
|
|
47
|
+
export function renderGithub(result) {
|
|
48
|
+
const level = (s) => (s === "error" ? "error" : s === "warn" ? "warning" : "notice");
|
|
49
|
+
const esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
50
|
+
const prop = (s) => esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
51
|
+
const lines = result.violations.map((v) => {
|
|
52
|
+
const fix = v.fix.replace !== null ? ` Fix (${v.fix.confidence}): ${v.fix.replace}` : "";
|
|
53
|
+
return `::${level(v.severity)} file=${prop(v.file)},line=${v.range.start.line},endLine=${v.range.end.line},col=${v.range.start.col},endColumn=${v.range.end.col},title=${prop(`zengin ${v.rule}`)}::${esc(v.message + fix)}`;
|
|
54
|
+
});
|
|
55
|
+
lines.push(`${result.filesChecked} files checked against ${result.system.package}@${result.system.version}: ${renderSummary(result.summary)}`);
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
export function renderJson(result) {
|
|
59
|
+
const { projectDir, configPath, system, filesChecked, summary, violations, exitCode } = result;
|
|
60
|
+
return JSON.stringify({ projectDir, configPath, system, filesChecked, exitCode, summary, violations }, null, 2);
|
|
61
|
+
}
|
|
62
|
+
function oneLine(s) {
|
|
63
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
64
|
+
return flat.length > 120 ? flat.slice(0, 117) + "..." : flat;
|
|
65
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Files added, copied, modified or renamed relative to `ref`, plus uncommitted and untracked changes. */
|
|
2
|
+
export declare function changedFiles(projectDir: string, ref: string): string[];
|
|
3
|
+
/** Files staged for the next commit. */
|
|
4
|
+
export declare function stagedFiles(projectDir: string): string[];
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
function git(projectDir, args) {
|
|
4
|
+
let out;
|
|
5
|
+
try {
|
|
6
|
+
out = execFileSync("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
const msg = e instanceof Error && "stderr" in e ? String(e.stderr).trim() : String(e);
|
|
10
|
+
throw new Error(`git ${args.join(" ")} failed in ${projectDir}: ${msg || "is this a git repository?"}`);
|
|
11
|
+
}
|
|
12
|
+
return out
|
|
13
|
+
.split("\n")
|
|
14
|
+
.map((l) => l.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* git reports paths relative to the repository root. The project may be a package inside a monorepo,
|
|
19
|
+
* so every path is re-based onto the project directory and anything outside it is dropped.
|
|
20
|
+
*/
|
|
21
|
+
function toProject(projectDir, rootRelative) {
|
|
22
|
+
const root = git(projectDir, ["rev-parse", "--show-toplevel"])[0];
|
|
23
|
+
return rootRelative
|
|
24
|
+
.map((p) => relative(projectDir, join(root, p)).replace(/\\/g, "/"))
|
|
25
|
+
.filter((p) => p && !p.startsWith("../") && p !== "..")
|
|
26
|
+
.sort();
|
|
27
|
+
}
|
|
28
|
+
/** Files added, copied, modified or renamed relative to `ref`, plus uncommitted and untracked changes. */
|
|
29
|
+
export function changedFiles(projectDir, ref) {
|
|
30
|
+
const base = ref || "HEAD";
|
|
31
|
+
const committed = git(projectDir, ["diff", "--name-only", "--diff-filter=ACMR", `${base}...HEAD`]);
|
|
32
|
+
const working = git(projectDir, ["diff", "--name-only", "--diff-filter=ACMR", "HEAD"]);
|
|
33
|
+
const untracked = git(projectDir, ["ls-files", "--others", "--exclude-standard", "--full-name"]);
|
|
34
|
+
return toProject(projectDir, [...new Set([...committed, ...working, ...untracked])]);
|
|
35
|
+
}
|
|
36
|
+
/** Files staged for the next commit. */
|
|
37
|
+
export function stagedFiles(projectDir) {
|
|
38
|
+
return toProject(projectDir, git(projectDir, ["diff", "--name-only", "--cached", "--diff-filter=ACMR"]));
|
|
39
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type CheckOptions } from "./check.js";
|
|
3
|
+
import { type ScaffoldOptions } from "./scaffold.js";
|
|
4
|
+
interface Parsed {
|
|
5
|
+
command: "check" | "explain" | "init" | "report" | "rollup" | "create" | "add" | "tokens" | "registry" | "theme" | "fonts" | "icons" | "upgrade" | "brand" | "figma" | "mock" | "help";
|
|
6
|
+
positional: string[];
|
|
7
|
+
check: CheckOptions;
|
|
8
|
+
init: {
|
|
9
|
+
from?: string;
|
|
10
|
+
dir?: string;
|
|
11
|
+
force: boolean;
|
|
12
|
+
};
|
|
13
|
+
report: {
|
|
14
|
+
repo?: string;
|
|
15
|
+
includeViolations: boolean;
|
|
16
|
+
out?: string;
|
|
17
|
+
into?: string;
|
|
18
|
+
at?: string;
|
|
19
|
+
commit?: string;
|
|
20
|
+
ref?: string;
|
|
21
|
+
};
|
|
22
|
+
rollup: {
|
|
23
|
+
previous?: string;
|
|
24
|
+
format: "markdown" | "json" | "html";
|
|
25
|
+
out?: string;
|
|
26
|
+
};
|
|
27
|
+
scaffold: ScaffoldOptions;
|
|
28
|
+
}
|
|
29
|
+
export declare function parseArgs(argv: string[], cwd: string): Parsed;
|
|
30
|
+
export declare function explain(rule: string | undefined): string;
|
|
31
|
+
export {};
|