synthesisui 0.7.0 → 0.8.1
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/commands/doctor.js +33 -1
- package/dist/doctor/overrides.js +128 -15
- package/dist/doctor/scan.js +52 -2
- package/dist/document-diff.js +30 -4
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
@@ -185,11 +196,12 @@ export async function doctor(opts) {
|
|
|
185
196
|
const root = resolve(opts.dir ?? process.cwd());
|
|
186
197
|
const { table, recipes } = await loadSystem(root);
|
|
187
198
|
const hasSystem = table.byName.size > 0;
|
|
199
|
+
const scopes = (opts.scopes ?? []).map((s) => resolve(root, s));
|
|
188
200
|
const reports = [];
|
|
189
201
|
const overrides = [];
|
|
190
202
|
const used = new Map();
|
|
191
203
|
const known = new Set(recipes.keys());
|
|
192
|
-
for await (const file of walk(root)) {
|
|
204
|
+
for await (const file of scopes.length > 0 ? walkAll(scopes) : walk(root)) {
|
|
193
205
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
194
206
|
if (!src)
|
|
195
207
|
continue;
|
|
@@ -212,6 +224,21 @@ export async function doctor(opts) {
|
|
|
212
224
|
console.log(body(hasSystem
|
|
213
225
|
? `${table.name ?? table.slug} v${table.version ?? "?"} - ${table.byName.size} tokens, ${d.scanned} files read`
|
|
214
226
|
: `No system installed - ${d.scanned} files read`));
|
|
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
|
+
}
|
|
215
242
|
// 0 of 0 is not a perfect score, it is an empty measurement - printing a
|
|
216
243
|
// full bar there would be the report's first lie.
|
|
217
244
|
const measurable = d.tokenUses + d.findings.length > 0;
|
|
@@ -281,8 +308,13 @@ export async function doctor(opts) {
|
|
|
281
308
|
console.log(body(file));
|
|
282
309
|
for (const o of opts.all ? list : list.slice(0, 3)) {
|
|
283
310
|
console.log(` ${String(o.line).padStart(4)} ds-${o.component} · ${o.prop}: ${o.wrote}`);
|
|
311
|
+
// Always say what is being overruled. A finding that cannot name it is
|
|
312
|
+
// indistinguishable from a bug in the reader's eyes, and one of these
|
|
313
|
+
// (`.ds-card:hover { transform: none }`) was a real, deliberate call.
|
|
284
314
|
if (o.recipe)
|
|
285
315
|
console.log(` the recipe binds ${o.recipe}`);
|
|
316
|
+
else if (o.where)
|
|
317
|
+
console.log(` the recipe binds it ${o.where}`);
|
|
286
318
|
}
|
|
287
319
|
if (!opts.all && list.length > 3) {
|
|
288
320
|
console.log(` +${list.length - 3} more`);
|
package/dist/doctor/overrides.js
CHANGED
|
@@ -46,10 +46,37 @@ export function bindingsOf(recipe) {
|
|
|
46
46
|
collect(r.variants, false);
|
|
47
47
|
collect(r.states, false);
|
|
48
48
|
collect(r.parts, false);
|
|
49
|
+
// Where a non-base property lives, so the report can name it. Base wins: if
|
|
50
|
+
// base binds it too, that value is the one worth showing.
|
|
51
|
+
const boundIn = new Map();
|
|
52
|
+
const note = (block, label) => {
|
|
53
|
+
if (!block || typeof block !== "object")
|
|
54
|
+
return;
|
|
55
|
+
for (const [k, v] of Object.entries(block)) {
|
|
56
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
57
|
+
const p = kebab(k);
|
|
58
|
+
if (!baseValues.has(p) && !boundIn.has(p))
|
|
59
|
+
boundIn.set(p, `${label}: ${v}`);
|
|
60
|
+
}
|
|
61
|
+
else if (v && typeof v === "object") {
|
|
62
|
+
note(v, label);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
for (const [state, block] of Object.entries((r.states ?? {}))) {
|
|
67
|
+
note(block, `on ${state}`);
|
|
68
|
+
}
|
|
69
|
+
for (const [axis, opts] of Object.entries((r.variants ?? {}))) {
|
|
70
|
+
if (!opts || typeof opts !== "object")
|
|
71
|
+
continue;
|
|
72
|
+
for (const [opt, block] of Object.entries(opts)) {
|
|
73
|
+
note(block, `on ${axis}="${opt}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
49
76
|
const usage = Array.isArray(r.usage)
|
|
50
77
|
? r.usage.filter((x) => typeof x === "string")
|
|
51
78
|
: [];
|
|
52
|
-
return { props, baseValues, usage };
|
|
79
|
+
return { props, baseValues, boundIn, usage };
|
|
53
80
|
}
|
|
54
81
|
/**
|
|
55
82
|
* How many places each known component is used. An inventory nobody has today:
|
|
@@ -117,6 +144,77 @@ function enclosingTag(src, at) {
|
|
|
117
144
|
return null;
|
|
118
145
|
}
|
|
119
146
|
const lineAt = (src, index) => src.slice(0, index).split("\n").length;
|
|
147
|
+
/**
|
|
148
|
+
* Whatever the `style` prop was given, with where it starts. The expression
|
|
149
|
+
* container, not `{{` specifically: a real component writes
|
|
150
|
+
* `style={showImg ? undefined : { color: "#fff" }}`, and requiring the double
|
|
151
|
+
* brace dropped that finding on the floor. Brace matching rather than a regex,
|
|
152
|
+
* because the value nests.
|
|
153
|
+
*/
|
|
154
|
+
function styleRegions(tag) {
|
|
155
|
+
const out = [];
|
|
156
|
+
const open = /style\s*=\s*\{/g;
|
|
157
|
+
let m = open.exec(tag);
|
|
158
|
+
while (m !== null) {
|
|
159
|
+
const start = m.index + m[0].length;
|
|
160
|
+
let depth = 1; // the container brace just consumed
|
|
161
|
+
let end = start;
|
|
162
|
+
for (; end < tag.length; end++) {
|
|
163
|
+
if (tag[end] === "{")
|
|
164
|
+
depth++;
|
|
165
|
+
else if (tag[end] === "}" && --depth === 0)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
// Only the OBJECT LITERALS inside the container. Scanning the container
|
|
169
|
+
// whole let a one-line ternary eat the declaration: in
|
|
170
|
+
// `style={on ? undefined : { padding: "7px" }}` the colon after `undefined`
|
|
171
|
+
// reads as a property separator and swallows the padding behind it.
|
|
172
|
+
const container = tag.slice(start, end);
|
|
173
|
+
let d = 0;
|
|
174
|
+
let objStart = -1;
|
|
175
|
+
for (let i = 0; i < container.length; i++) {
|
|
176
|
+
if (container[i] === "{") {
|
|
177
|
+
if (d === 0)
|
|
178
|
+
objStart = i + 1;
|
|
179
|
+
d++;
|
|
180
|
+
}
|
|
181
|
+
else if (container[i] === "}" && d > 0 && --d === 0) {
|
|
182
|
+
out.push({
|
|
183
|
+
text: container.slice(objStart, i),
|
|
184
|
+
offset: start + objStart,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// `style={{ … }}` - the container IS the object
|
|
189
|
+
if (objStart === -1)
|
|
190
|
+
out.push({ text: container, offset: start });
|
|
191
|
+
open.lastIndex = end;
|
|
192
|
+
m = open.exec(tag);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The literal a style property was given, or null when it was given an
|
|
198
|
+
* expression instead.
|
|
199
|
+
*
|
|
200
|
+
* `style={{ width: w }}` passes a variable; `transform: shown ? "a" : "b"`
|
|
201
|
+
* passes a ternary. Neither is a value the author hardcoded over the recipe,
|
|
202
|
+
* and reporting them as "the recipe binds 100%" was noise on a real project
|
|
203
|
+
* (investidorez, 25/07). Only a quoted string or a bare number is an override
|
|
204
|
+
* anyone can act on.
|
|
205
|
+
*/
|
|
206
|
+
function literalValue(raw) {
|
|
207
|
+
const v = raw.trim();
|
|
208
|
+
const quoted = /^(["'])([\s\S]*)\1$/.exec(v);
|
|
209
|
+
if (quoted) {
|
|
210
|
+
// a template that interpolates is decided at runtime, not here
|
|
211
|
+
return quoted[2].includes("${") ? null : quoted[2];
|
|
212
|
+
}
|
|
213
|
+
if (v.startsWith("`"))
|
|
214
|
+
return null;
|
|
215
|
+
// bare numbers are valid in a JSX style object (padding: 8)
|
|
216
|
+
return /^-?\d*\.?\d+(px|rem|em|%|vh|vw|ch|s|ms)?$/.test(v) ? v : null;
|
|
217
|
+
}
|
|
120
218
|
export function findOverrides(source, recipes) {
|
|
121
219
|
const out = [];
|
|
122
220
|
const lines = source.split("\n");
|
|
@@ -137,19 +235,29 @@ export function findOverrides(source, recipes) {
|
|
|
137
235
|
const tag = enclosingTag(source, m.index ?? 0);
|
|
138
236
|
if (!tag)
|
|
139
237
|
continue;
|
|
140
|
-
// inline style={{ borderRadius: 4 }}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
prop
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
238
|
+
// inline style={{ borderRadius: 4 }} - and ONLY that. Reading the whole tag
|
|
239
|
+
// meant reading every object prop on it, so Framer Motion's
|
|
240
|
+
// `animate={{ opacity: 1 }}` came back as "ds-card overrules opacity"
|
|
241
|
+
// (investidorez, 25/07). An animation is not a design decision.
|
|
242
|
+
for (const region of styleRegions(tag.text)) {
|
|
243
|
+
for (const s of region.text.matchAll(/([a-zA-Z-]+)\s*:\s*("[^"]*"|'[^']*'|[^,}\n]+)/g)) {
|
|
244
|
+
const prop = kebab(s[1]);
|
|
245
|
+
if (!bind.props.has(prop))
|
|
246
|
+
continue;
|
|
247
|
+
const wrote = literalValue(s[2]);
|
|
248
|
+
if (wrote === null)
|
|
249
|
+
continue;
|
|
250
|
+
const at = tag.start + region.offset + (s.index ?? 0);
|
|
251
|
+
record({
|
|
252
|
+
component: name,
|
|
253
|
+
prop,
|
|
254
|
+
wrote,
|
|
255
|
+
recipe: bind.baseValues.get(prop) ?? null,
|
|
256
|
+
where: bind.boundIn.get(prop) ?? null,
|
|
257
|
+
line: lineAt(source, at),
|
|
258
|
+
excerpt: clip(lines[lineAt(source, at) - 1]?.trim() ?? ""),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
153
261
|
}
|
|
154
262
|
// utility classes sitting beside the ds-* class
|
|
155
263
|
for (const cls of tag.text.matchAll(/[\w:[\]#().%/-]+/g)) {
|
|
@@ -161,6 +269,7 @@ export function findOverrides(source, recipes) {
|
|
|
161
269
|
prop,
|
|
162
270
|
wrote: cls[0],
|
|
163
271
|
recipe: bind.baseValues.get(prop) ?? null,
|
|
272
|
+
where: bind.boundIn.get(prop) ?? null,
|
|
164
273
|
line: lineAt(source, tag.start + (cls.index ?? 0)),
|
|
165
274
|
excerpt: clip(lines[lineAt(source, tag.start + (cls.index ?? 0)) - 1]?.trim() ?? ""),
|
|
166
275
|
});
|
|
@@ -168,7 +277,10 @@ export function findOverrides(source, recipes) {
|
|
|
168
277
|
}
|
|
169
278
|
// ── CSS: a rule whose selector reaches a ds-* component ───────────────────
|
|
170
279
|
for (const rule of source.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
171
|
-
|
|
280
|
+
// A CSS comment is not a selector. `/* botões (.ds-button) … */` above an
|
|
281
|
+
// unrelated rule made the doctor attribute that rule to ds-button - it was
|
|
282
|
+
// reading prose as structure (investidorez, 25/07).
|
|
283
|
+
const selector = rule[1].replace(/\/\*[\s\S]*?\*\//g, "");
|
|
172
284
|
const hit = /\.ds-([a-z][a-z0-9-]*)\b/.exec(selector);
|
|
173
285
|
const bind = hit ? recipes.get(hit[1]) : undefined;
|
|
174
286
|
if (!hit || !bind)
|
|
@@ -186,6 +298,7 @@ export function findOverrides(source, recipes) {
|
|
|
186
298
|
prop,
|
|
187
299
|
wrote: decl[2].trim(),
|
|
188
300
|
recipe: bind.baseValues.get(prop) ?? null,
|
|
301
|
+
where: bind.boundIn.get(prop) ?? null,
|
|
189
302
|
line: lineAt(source, at),
|
|
190
303
|
excerpt: clip(lines[lineAt(source, at) - 1]?.trim() ?? ""),
|
|
191
304
|
});
|
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,7 +26,7 @@ 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 [
|
|
29
|
+
synthesisui doctor [paths…] [--all] audit for DRIFT: every design value written by
|
|
30
30
|
hand, the token your system already has for it, and the
|
|
31
31
|
laws your system carries for what you use
|
|
32
32
|
|
|
@@ -63,6 +63,7 @@ Examples:
|
|
|
63
63
|
synthesisui init --target next
|
|
64
64
|
synthesisui init --target next --ds halogen bootstrap + bring a system in
|
|
65
65
|
synthesisui doctor
|
|
66
|
+
synthesisui doctor apps/web packages/ui # scope the read in a monorepo
|
|
66
67
|
synthesisui doctor --strict
|
|
67
68
|
synthesisui list
|
|
68
69
|
synthesisui add halogen
|
|
@@ -120,8 +121,11 @@ async function main() {
|
|
|
120
121
|
const dir = typeof flags.dir === "string" ? flags.dir : undefined;
|
|
121
122
|
switch (command) {
|
|
122
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
|
|
123
126
|
await doctor({
|
|
124
127
|
dir,
|
|
128
|
+
scopes: args,
|
|
125
129
|
strict: flags.strict === true,
|
|
126
130
|
all: flags.all === true,
|
|
127
131
|
laws: flags.laws === true,
|
package/package.json
CHANGED