synthesisui 0.6.0 → 0.8.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 +22 -2
- package/dist/commands/doctor.js +81 -6
- package/dist/doctor/overrides.js +44 -2
- package/dist/doctor/scan.js +52 -2
- package/dist/document-diff.js +30 -4
- package/dist/index.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,8 +99,28 @@ worse than none.
|
|
|
99
99
|
With no system installed it still finds every hand-written value and counts
|
|
100
100
|
the distinct ones. Runs offline, needs no account, writes nothing.
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
### What your system says
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
── What your system says about what you use ──────────────────────
|
|
106
|
+
|
|
107
|
+
ds-badge · 3 places
|
|
108
|
+
Badges whisper status in periwinkle; danger appears only for money at risk.
|
|
109
|
+
|
|
110
|
+
ds-button · 3 places
|
|
111
|
+
One indigo action per view; gold is reserved for moments of ceremony, never buttons.
|
|
112
|
+
Buttons speak quietly - sentence case, no exclamation.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The usage laws your team wrote, for the components this project actually uses,
|
|
116
|
+
ordered by how much you use them. They are prose, so nothing verifies them -
|
|
117
|
+
the value is putting them in front of whoever is touching the component. No
|
|
118
|
+
other tool is positioned to do it, because no other tool knows these laws
|
|
119
|
+
exist.
|
|
120
|
+
|
|
121
|
+
`--strict` exits 1 when drift is found, for CI. `--all` lists every finding
|
|
122
|
+
instead of the loudest files. `--laws` shows every law instead of the busiest
|
|
123
|
+
components.
|
|
104
124
|
|
|
105
125
|
## What `add` materializes
|
|
106
126
|
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
-
import { bindingsFromDocument, findOverrides, } from "../doctor/overrides.js";
|
|
3
|
+
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
4
4
|
import { diagnose, scanSource, } from "../doctor/scan.js";
|
|
5
5
|
import { buildTable, EMPTY_TABLE } from "../doctor/tokens.js";
|
|
6
6
|
import { body, section, snippet } from "../output.js";
|
|
@@ -59,6 +59,17 @@ async function* walk(dir) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/** The same walk over several roots, in the order the caller named them. A
|
|
63
|
+
* single file passed as a scope is read as itself. */
|
|
64
|
+
async function* walkAll(roots) {
|
|
65
|
+
for (const r of roots) {
|
|
66
|
+
if (EXTS.some((x) => r.endsWith(x))) {
|
|
67
|
+
yield r;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
yield* walk(r);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
62
73
|
/** Find the installed system: `_synthesisui/ds/<slug>/tokens.css` + `.lock`,
|
|
63
74
|
* and the recipes `add` put next to them in `design-system.json`. Those
|
|
64
75
|
* recipes are why the component pass can exist at all - a linter has no idea
|
|
@@ -132,7 +143,7 @@ function meter(pct, width = 24) {
|
|
|
132
143
|
const filled = Math.round((pct / 100) * width);
|
|
133
144
|
return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
134
145
|
}
|
|
135
|
-
function verdict(d, hasSystem) {
|
|
146
|
+
function verdict(d, hasSystem, overruled) {
|
|
136
147
|
// Nothing found and nothing installed: a utility package, a config folder,
|
|
137
148
|
// the wrong directory. Selling a design system here would be noise.
|
|
138
149
|
if (!hasSystem && d.findings.length === 0) {
|
|
@@ -152,12 +163,26 @@ function verdict(d, hasSystem) {
|
|
|
152
163
|
body("Browse systems at https://www.synthesisui.com/gallery"),
|
|
153
164
|
];
|
|
154
165
|
}
|
|
155
|
-
if (d.findings.length === 0) {
|
|
166
|
+
if (d.findings.length === 0 && overruled === 0) {
|
|
156
167
|
return [
|
|
157
168
|
body("No drift. Every design value in this project comes from the"),
|
|
158
169
|
body("system. That is a rarer sentence than it sounds."),
|
|
159
170
|
];
|
|
160
171
|
}
|
|
172
|
+
// A report that accuses a component of being overruled and then closes with
|
|
173
|
+
// "no drift" is a report nobody believes twice.
|
|
174
|
+
if (d.findings.length === 0) {
|
|
175
|
+
return [
|
|
176
|
+
body("No loose values - everything is a token."),
|
|
177
|
+
body(overruled === 1
|
|
178
|
+
? "But one place takes a component the system defines and"
|
|
179
|
+
: `But ${overruled} places take a component the system defines and`),
|
|
180
|
+
body(overruled === 1
|
|
181
|
+
? "overrules it locally. Either the recipe should change, or"
|
|
182
|
+
: "overrule it locally. Either the recipe should change, or"),
|
|
183
|
+
body("that override should not be there."),
|
|
184
|
+
];
|
|
185
|
+
}
|
|
161
186
|
const lines = [
|
|
162
187
|
body(`${d.named} of ${d.findings.length} already have a name in your system.`),
|
|
163
188
|
body("Those are the cheap ones: swap the literal for the token."),
|
|
@@ -171,9 +196,12 @@ export async function doctor(opts) {
|
|
|
171
196
|
const root = resolve(opts.dir ?? process.cwd());
|
|
172
197
|
const { table, recipes } = await loadSystem(root);
|
|
173
198
|
const hasSystem = table.byName.size > 0;
|
|
199
|
+
const scopes = (opts.scopes ?? []).map((s) => resolve(root, s));
|
|
174
200
|
const reports = [];
|
|
175
201
|
const overrides = [];
|
|
176
|
-
|
|
202
|
+
const used = new Map();
|
|
203
|
+
const known = new Set(recipes.keys());
|
|
204
|
+
for await (const file of scopes.length > 0 ? walkAll(scopes) : walk(root)) {
|
|
177
205
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
178
206
|
if (!src)
|
|
179
207
|
continue;
|
|
@@ -182,6 +210,9 @@ export async function doctor(opts) {
|
|
|
182
210
|
if (recipes.size > 0) {
|
|
183
211
|
for (const o of findOverrides(src, recipes))
|
|
184
212
|
overrides.push({ ...o, file: rel });
|
|
213
|
+
for (const [name, n] of countComponents(src, known)) {
|
|
214
|
+
used.set(name, (used.get(name) ?? 0) + n);
|
|
215
|
+
}
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
if (reports.length === 0) {
|
|
@@ -193,7 +224,25 @@ export async function doctor(opts) {
|
|
|
193
224
|
console.log(body(hasSystem
|
|
194
225
|
? `${table.name ?? table.slug} v${table.version ?? "?"} - ${table.byName.size} tokens, ${d.scanned} files read`
|
|
195
226
|
: `No system installed - ${d.scanned} files read`));
|
|
196
|
-
if (
|
|
227
|
+
if (scopes.length > 0) {
|
|
228
|
+
console.log(body(`scope: ${opts.scopes?.join(", ")}`));
|
|
229
|
+
}
|
|
230
|
+
// What a token could never have held, said out loud. A report that quietly
|
|
231
|
+
// drops things is only trustworthy until someone notices, and then it is
|
|
232
|
+
// worth less than one that says too much.
|
|
233
|
+
const aside = new Map();
|
|
234
|
+
for (const r of reports) {
|
|
235
|
+
if (!r.setAside)
|
|
236
|
+
continue;
|
|
237
|
+
aside.set(r.setAside.reason, (aside.get(r.setAside.reason) ?? 0) + r.setAside.count);
|
|
238
|
+
}
|
|
239
|
+
for (const [reason, count] of aside) {
|
|
240
|
+
console.log(body(`set aside: ${count} value(s) in ${reason}`));
|
|
241
|
+
}
|
|
242
|
+
// 0 of 0 is not a perfect score, it is an empty measurement - printing a
|
|
243
|
+
// full bar there would be the report's first lie.
|
|
244
|
+
const measurable = d.tokenUses + d.findings.length > 0;
|
|
245
|
+
if (hasSystem && measurable) {
|
|
197
246
|
console.log("");
|
|
198
247
|
console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
|
|
199
248
|
console.log(body(` ${d.tokenUses} from the system, ${d.findings.length} by hand`));
|
|
@@ -271,8 +320,34 @@ export async function doctor(opts) {
|
|
|
271
320
|
console.log(body(`+${byFile.size - shown.length} more files. Run with --all.`));
|
|
272
321
|
}
|
|
273
322
|
}
|
|
323
|
+
// The system's own words, for the components this project actually uses.
|
|
324
|
+
// Prose, so nothing verifies it - the value is putting it in front of
|
|
325
|
+
// whoever is touching the component, which no other tool is positioned to
|
|
326
|
+
// do because no other tool knows these laws exist.
|
|
327
|
+
const inUse = [...used.entries()]
|
|
328
|
+
.filter(([name]) => (recipes.get(name)?.usage.length ?? 0) > 0)
|
|
329
|
+
.sort((a, b) => b[1] - a[1]);
|
|
330
|
+
if (inUse.length > 0) {
|
|
331
|
+
console.log(section("What your system says about what you use"));
|
|
332
|
+
const shownComps = opts.laws || opts.all ? inUse : inUse.slice(0, 4);
|
|
333
|
+
for (const [name, count] of shownComps) {
|
|
334
|
+
const laws = recipes.get(name)?.usage ?? [];
|
|
335
|
+
const shown = opts.laws || opts.all ? laws : laws.slice(0, 2);
|
|
336
|
+
console.log(body(`ds-${name} · ${count} place${count === 1 ? "" : "s"}`));
|
|
337
|
+
for (const law of shown)
|
|
338
|
+
console.log(` ${law}`);
|
|
339
|
+
if (laws.length > shown.length) {
|
|
340
|
+
console.log(` +${laws.length - shown.length} more`);
|
|
341
|
+
}
|
|
342
|
+
console.log("");
|
|
343
|
+
}
|
|
344
|
+
if (inUse.length > shownComps.length) {
|
|
345
|
+
console.log(body(`+${inUse.length - shownComps.length} more components carry laws. Run with --laws.`));
|
|
346
|
+
console.log("");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
274
349
|
console.log(section("What this means"));
|
|
275
|
-
for (const line of verdict(d, hasSystem))
|
|
350
|
+
for (const line of verdict(d, hasSystem, overrides.length))
|
|
276
351
|
console.log(line);
|
|
277
352
|
console.log("");
|
|
278
353
|
if (opts.strict && (d.findings.length > 0 || overrides.length > 0))
|
package/dist/doctor/overrides.js
CHANGED
|
@@ -46,7 +46,24 @@ export function bindingsOf(recipe) {
|
|
|
46
46
|
collect(r.variants, false);
|
|
47
47
|
collect(r.states, false);
|
|
48
48
|
collect(r.parts, false);
|
|
49
|
-
|
|
49
|
+
const usage = Array.isArray(r.usage)
|
|
50
|
+
? r.usage.filter((x) => typeof x === "string")
|
|
51
|
+
: [];
|
|
52
|
+
return { props, baseValues, usage };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* How many places each known component is used. An inventory nobody has today:
|
|
56
|
+
* a design system's own report can say "ds-button, 14 places" because it knows
|
|
57
|
+
* which names are components rather than guessing at class strings.
|
|
58
|
+
*/
|
|
59
|
+
export function countComponents(source, known) {
|
|
60
|
+
const out = new Map();
|
|
61
|
+
for (const m of source.matchAll(DS_IN_CLASS)) {
|
|
62
|
+
if (!known.has(m[1]))
|
|
63
|
+
continue;
|
|
64
|
+
out.set(m[1], (out.get(m[1]) ?? 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
50
67
|
}
|
|
51
68
|
/**
|
|
52
69
|
* Tailwind utilities that plainly set a CSS property. Deliberately short:
|
|
@@ -100,6 +117,28 @@ function enclosingTag(src, at) {
|
|
|
100
117
|
return null;
|
|
101
118
|
}
|
|
102
119
|
const lineAt = (src, index) => src.slice(0, index).split("\n").length;
|
|
120
|
+
/**
|
|
121
|
+
* The literal a style property was given, or null when it was given an
|
|
122
|
+
* expression instead.
|
|
123
|
+
*
|
|
124
|
+
* `style={{ width: w }}` passes a variable; `transform: shown ? "a" : "b"`
|
|
125
|
+
* passes a ternary. Neither is a value the author hardcoded over the recipe,
|
|
126
|
+
* and reporting them as "the recipe binds 100%" was noise on a real project
|
|
127
|
+
* (investidorez, 25/07). Only a quoted string or a bare number is an override
|
|
128
|
+
* anyone can act on.
|
|
129
|
+
*/
|
|
130
|
+
function literalValue(raw) {
|
|
131
|
+
const v = raw.trim();
|
|
132
|
+
const quoted = /^(["'])([\s\S]*)\1$/.exec(v);
|
|
133
|
+
if (quoted) {
|
|
134
|
+
// a template that interpolates is decided at runtime, not here
|
|
135
|
+
return quoted[2].includes("${") ? null : quoted[2];
|
|
136
|
+
}
|
|
137
|
+
if (v.startsWith("`"))
|
|
138
|
+
return null;
|
|
139
|
+
// bare numbers are valid in a JSX style object (padding: 8)
|
|
140
|
+
return /^-?\d*\.?\d+(px|rem|em|%|vh|vw|ch|s|ms)?$/.test(v) ? v : null;
|
|
141
|
+
}
|
|
103
142
|
export function findOverrides(source, recipes) {
|
|
104
143
|
const out = [];
|
|
105
144
|
const lines = source.split("\n");
|
|
@@ -125,10 +164,13 @@ export function findOverrides(source, recipes) {
|
|
|
125
164
|
const prop = kebab(s[1]);
|
|
126
165
|
if (!bind.props.has(prop))
|
|
127
166
|
continue;
|
|
167
|
+
const wrote = literalValue(s[2]);
|
|
168
|
+
if (wrote === null)
|
|
169
|
+
continue;
|
|
128
170
|
record({
|
|
129
171
|
component: name,
|
|
130
172
|
prop,
|
|
131
|
-
wrote
|
|
173
|
+
wrote,
|
|
132
174
|
recipe: bind.baseValues.get(prop) ?? null,
|
|
133
175
|
line: lineAt(source, tag.start + (s.index ?? 0)),
|
|
134
176
|
excerpt: clip(lines[lineAt(source, tag.start + (s.index ?? 0)) - 1]?.trim() ?? ""),
|
package/dist/doctor/scan.js
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
* takes eight seconds and needs a build step is a diagnosis nobody runs.
|
|
11
11
|
*/
|
|
12
12
|
import { tokenFor } from "./tokens.js";
|
|
13
|
+
/**
|
|
14
|
+
* `next/og` renders JSX to a PNG on the server. There is no document, so there
|
|
15
|
+
* is no `var(--ds-*)` to read: every colour in such a file MUST be a literal.
|
|
16
|
+
* Telling the author to tokenize it is telling them to break the build
|
|
17
|
+
* (investidorez, 25/07 - two files, nineteen impossible findings).
|
|
18
|
+
*/
|
|
19
|
+
const RENDERS_IMAGE = /from\s+["']next\/og["']|\bnew\s+ImageResponse\b/;
|
|
20
|
+
/** A colour sitting on an SVG paint attribute is artwork - a brand mark, an
|
|
21
|
+
* illustration. Google's logo contributed four "fix me" lines to a real
|
|
22
|
+
* report; no design system should own those four hexes. */
|
|
23
|
+
const SVG_PAINT = /(?:fill|stroke)\s*[=:]\s*["']?$/;
|
|
13
24
|
const clip = (s) => (s.length > 84 ? `${s.slice(0, 81)}...` : s);
|
|
14
25
|
/** Lines we must not read as authorship: imports, and our own installed CSS. */
|
|
15
26
|
const IGNORE_LINE = /^\s*(import|@import|\/\/|\*|\/\*)/;
|
|
@@ -34,10 +45,32 @@ const IDIOM = new Set(["0", "0px", "1px", "9999px", "100%", "50%"]);
|
|
|
34
45
|
export function scanSource(file, source, table) {
|
|
35
46
|
const findings = [];
|
|
36
47
|
let tokenUses = 0;
|
|
48
|
+
let aside = 0;
|
|
49
|
+
if (RENDERS_IMAGE.test(source)) {
|
|
50
|
+
return {
|
|
51
|
+
file,
|
|
52
|
+
findings: [],
|
|
53
|
+
tokenUses: 0,
|
|
54
|
+
setAside: {
|
|
55
|
+
reason: "renders to an image, where CSS variables do not exist",
|
|
56
|
+
count: (source.match(COLOR) ?? []).length,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let svgDepth = 0;
|
|
37
61
|
source.split("\n").forEach((raw, i) => {
|
|
38
62
|
const line = raw.trim();
|
|
39
63
|
const at = i + 1;
|
|
40
64
|
tokenUses += (line.match(TOKEN_USE) ?? []).length;
|
|
65
|
+
// Depth at the START of this line, carried before the early return so a
|
|
66
|
+
// blank line inside an <svg> cannot close the region by accident. The
|
|
67
|
+
// per-match depth is recomputed below, because an icon is often written on
|
|
68
|
+
// one line - `<svg><path fill="#4285F4"/></svg>` opens and closes around
|
|
69
|
+
// the value, and a line-level counter reads zero right where it matters.
|
|
70
|
+
const lineStart = svgDepth;
|
|
71
|
+
svgDepth = Math.max(0, svgDepth +
|
|
72
|
+
(line.match(/<svg\b/g) ?? []).length -
|
|
73
|
+
(line.match(/<\/svg>/g) ?? []).length);
|
|
41
74
|
if (!line || IGNORE_LINE.test(line))
|
|
42
75
|
return;
|
|
43
76
|
// `rgba(${r}, ${g}, ${b}, ${a})` is code computing a colour, not a colour
|
|
@@ -60,8 +93,18 @@ export function scanSource(file, source, table) {
|
|
|
60
93
|
excerpt: clip(line),
|
|
61
94
|
});
|
|
62
95
|
};
|
|
63
|
-
for (const m of line.matchAll(COLOR))
|
|
96
|
+
for (const m of line.matchAll(COLOR)) {
|
|
97
|
+
const before = line.slice(0, m.index ?? 0);
|
|
98
|
+
const depthHere = lineStart +
|
|
99
|
+
(before.match(/<svg\b/g) ?? []).length -
|
|
100
|
+
(before.match(/<\/svg>/g) ?? []).length;
|
|
101
|
+
// inside an <svg>, a colour on fill= or stroke= is paint, not surface
|
|
102
|
+
if (depthHere > 0 && SVG_PAINT.test(before)) {
|
|
103
|
+
aside++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
64
106
|
push("color", m[0]);
|
|
107
|
+
}
|
|
65
108
|
for (const m of line.matchAll(RADIUS)) {
|
|
66
109
|
const value = `${m[1]}${m[2]}`;
|
|
67
110
|
if (!IDIOM.has(value))
|
|
@@ -80,7 +123,14 @@ export function scanSource(file, source, table) {
|
|
|
80
123
|
push("font", stack);
|
|
81
124
|
}
|
|
82
125
|
});
|
|
83
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
file,
|
|
128
|
+
findings,
|
|
129
|
+
tokenUses,
|
|
130
|
+
...(aside > 0
|
|
131
|
+
? { setAside: { reason: "SVG artwork", count: aside } }
|
|
132
|
+
: null),
|
|
133
|
+
};
|
|
84
134
|
}
|
|
85
135
|
export function diagnose(files) {
|
|
86
136
|
const flat = files.flatMap((f) => f.findings.map((x) => ({ ...x, file: f.file })));
|
package/dist/document-diff.js
CHANGED
|
@@ -46,7 +46,30 @@ function deepEqual(a, b) {
|
|
|
46
46
|
}
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Did this variant option ever put a declaration on the page? An option like
|
|
51
|
+
* `{ "tone": { "default": {} } }` compiles to nothing, so nothing in the
|
|
52
|
+
* consumer's app can depend on it.
|
|
53
|
+
*/
|
|
54
|
+
function declaresAnything(node) {
|
|
55
|
+
if (node === null || node === undefined)
|
|
56
|
+
return false;
|
|
57
|
+
if (typeof node !== "object")
|
|
58
|
+
return String(node).trim().length > 0;
|
|
59
|
+
if (Array.isArray(node))
|
|
60
|
+
return node.some(declaresAnything);
|
|
61
|
+
return Object.values(node).some(declaresAnything);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Variant options that existed, disappeared, AND used to render something.
|
|
65
|
+
*
|
|
66
|
+
* Vesper v2 → v15 announced ten breaking changes ("input no longer supports
|
|
67
|
+
* tone", and nine more). Every one of them was `{ tone: { default: {} } }` - an
|
|
68
|
+
* axis with a single empty option that emitted zero CSS. Removing it could not
|
|
69
|
+
* break an app, but the brief sent the consumer's agent hunting for `tone=`
|
|
70
|
+
* props that never existed (investidorez, 25/07). Housekeeping and a breaking
|
|
71
|
+
* change now read differently, because they are different.
|
|
72
|
+
*/
|
|
50
73
|
function removedVariantOptions(before, after) {
|
|
51
74
|
const out = [];
|
|
52
75
|
const prevVariants = (before.variants ?? {});
|
|
@@ -54,11 +77,14 @@ function removedVariantOptions(before, after) {
|
|
|
54
77
|
for (const [axis, options] of Object.entries(prevVariants)) {
|
|
55
78
|
const nextAxis = nextVariants[axis];
|
|
56
79
|
if (!nextAxis) {
|
|
57
|
-
|
|
80
|
+
if (declaresAnything(options))
|
|
81
|
+
out.push(axis);
|
|
58
82
|
continue;
|
|
59
83
|
}
|
|
60
|
-
for (const option of Object.
|
|
61
|
-
if (
|
|
84
|
+
for (const [option, decls] of Object.entries(options)) {
|
|
85
|
+
if (option in nextAxis)
|
|
86
|
+
continue;
|
|
87
|
+
if (declaresAnything(decls))
|
|
62
88
|
out.push(`${axis}="${option}"`);
|
|
63
89
|
}
|
|
64
90
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,8 +26,9 @@ Usage - deterministic, FREE:
|
|
|
26
26
|
synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
|
|
27
27
|
synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
|
|
28
28
|
synthesisui clean [--force] strip create-next-app boilerplate (dry run without --force)
|
|
29
|
-
synthesisui doctor [
|
|
30
|
-
hand,
|
|
29
|
+
synthesisui doctor [paths…] [--all] audit for DRIFT: every design value written by
|
|
30
|
+
hand, the token your system already has for it, and the
|
|
31
|
+
laws your system carries for what you use
|
|
31
32
|
|
|
32
33
|
Usage - AI, USES CREDITS (login required):
|
|
33
34
|
synthesisui generate "<desc>" AI-create a NEW component your DS doesn't have (token-only recipe)
|
|
@@ -53,6 +54,7 @@ Options:
|
|
|
53
54
|
--force clean: apply the changes (without it, dry run)
|
|
54
55
|
--strict doctor: exit 1 when drift is found (for CI)
|
|
55
56
|
--all doctor: list every finding, not just the loudest files
|
|
57
|
+
--laws doctor: show every usage law, not just the busiest components
|
|
56
58
|
--out <path> output path for the generated template (default: <pagesDir>/<file>)
|
|
57
59
|
-h, --help this help
|
|
58
60
|
|
|
@@ -61,6 +63,7 @@ Examples:
|
|
|
61
63
|
synthesisui init --target next
|
|
62
64
|
synthesisui init --target next --ds halogen bootstrap + bring a system in
|
|
63
65
|
synthesisui doctor
|
|
66
|
+
synthesisui doctor apps/web packages/ui # scope the read in a monorepo
|
|
64
67
|
synthesisui doctor --strict
|
|
65
68
|
synthesisui list
|
|
66
69
|
synthesisui add halogen
|
|
@@ -118,10 +121,14 @@ async function main() {
|
|
|
118
121
|
const dir = typeof flags.dir === "string" ? flags.dir : undefined;
|
|
119
122
|
switch (command) {
|
|
120
123
|
case "doctor":
|
|
124
|
+
// positional paths scope the READING (the system is still found from the
|
|
125
|
+
// root): `doctor apps/web packages/ui` in a monorepo
|
|
121
126
|
await doctor({
|
|
122
127
|
dir,
|
|
128
|
+
scopes: args,
|
|
123
129
|
strict: flags.strict === true,
|
|
124
130
|
all: flags.all === true,
|
|
131
|
+
laws: flags.laws === true,
|
|
125
132
|
});
|
|
126
133
|
break;
|
|
127
134
|
case "list":
|
package/package.json
CHANGED