synthesisui 0.13.0 → 0.15.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/claude-md.js +30 -8
- package/dist/commands/add.js +6 -0
- package/dist/commands/adopt.js +207 -0
- package/dist/commands/doctor.js +107 -6
- package/dist/doctor/tokens.js +3 -1
- package/dist/index.js +10 -0
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -18,7 +18,12 @@ async function readInstalled(projectRoot) {
|
|
|
18
18
|
try {
|
|
19
19
|
const raw = await readFile(join(dsDir, slug, ".lock"), "utf8");
|
|
20
20
|
const lock = JSON.parse(raw);
|
|
21
|
-
locks.push({
|
|
21
|
+
locks.push({
|
|
22
|
+
slug: lock.slug,
|
|
23
|
+
name: lock.name,
|
|
24
|
+
version: lock.version,
|
|
25
|
+
adopted: lock.adopted === true,
|
|
26
|
+
});
|
|
22
27
|
}
|
|
23
28
|
catch {
|
|
24
29
|
// folder without a valid .lock - ignore
|
|
@@ -87,24 +92,41 @@ async function renderRegion(projectRoot, installed) {
|
|
|
87
92
|
}
|
|
88
93
|
const sections = [];
|
|
89
94
|
for (const ds of installed) {
|
|
90
|
-
|
|
95
|
+
// An adopted system has no published version and therefore no `v<n>/`
|
|
96
|
+
// folder - pointing the agent at one points it at a file that does not
|
|
97
|
+
// exist (caught by running it, 27/07).
|
|
98
|
+
const head = ds.adopted
|
|
99
|
+
? `- **${ds.name}** (\`${ds.slug}\`, adopted from this repo) - guide: \`_synthesisui/ds/${ds.slug}/GUIDE.md\``
|
|
100
|
+
: `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) - guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``;
|
|
91
101
|
const manifest = await readManifest(projectRoot, ds);
|
|
92
102
|
sections.push(manifest ? `${head}\n${manifest}` : head);
|
|
93
103
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
// Two truths, and asserting the wrong one misleads the agent every time it
|
|
105
|
+
// writes a line. An INSTALLED system speaks `--ds-*` and is scoped with
|
|
106
|
+
// `data-ds`; an ADOPTED one is the project's own vocabulary, already wired,
|
|
107
|
+
// with no `data-ds` anywhere to scope to.
|
|
108
|
+
const onlyAdopted = installed.every((d) => d.adopted);
|
|
109
|
+
const rule = onlyAdopted
|
|
110
|
+
? `**When creating or editing components, read the system's GUIDE.md and follow it:** use the
|
|
111
|
+
project's OWN custom properties, exactly as the guide lists them. Do not write raw colours,
|
|
112
|
+
spacings or radii that a token already covers, and do not invent a new token silently - say so
|
|
113
|
+
instead, because a new token is a decision for a person to make. There is no component
|
|
114
|
+
manifest for an adopted system - the tokens ARE the contract. Run \`synthesisui doctor\` to see
|
|
115
|
+
how much of the shipped UI already resolves to them.`
|
|
116
|
+
: `**When creating or editing components, read the system's GUIDE.md and follow it:** use only semantic tokens
|
|
98
117
|
(\`var(--ds-color-semantic-*)\`, \`--ds-spacing-*\`, etc.), scope the UI with \`data-ds="<slug>"\`,
|
|
99
118
|
and reuse the \`.ds-*\` classes. Do not use raw values outside the system's scale. **Before
|
|
100
119
|
creating any UI element, check the component manifest below - if it exists, use or extend
|
|
101
120
|
it (\`synthesisui component <slug> <name>\` materializes it as your code).** To review a
|
|
102
121
|
component, create an isolated sample page (e.g. \`app/synthesisui-samples/<component>/\`) - do not
|
|
103
|
-
apply it to real production pages unless asked
|
|
122
|
+
apply it to real production pages unless asked.`;
|
|
123
|
+
const body = `## Design Systems (via SynthesisUI)
|
|
124
|
+
|
|
125
|
+
This project uses design system(s) tracked by the \`synthesisui\` CLI. ${rule}
|
|
104
126
|
|
|
105
127
|
${sections.join("\n")}
|
|
106
128
|
|
|
107
|
-
_Block managed by the CLI - do not edit by hand; run \`synthesisui add <slug
|
|
129
|
+
_Block managed by the CLI - do not edit by hand; run \`synthesisui ${onlyAdopted ? "adopt --write" : "add <slug>"}\` to update._`;
|
|
108
130
|
return `${START}\n${body}\n${END}`;
|
|
109
131
|
}
|
|
110
132
|
/**
|
package/dist/commands/add.js
CHANGED
|
@@ -206,6 +206,12 @@ export async function add(slug, opts) {
|
|
|
206
206
|
console.log(line(` Prefer next/font or self-hosting? Fine - just register these exact families: ${customFontFamilies(families).join(", ")}.`));
|
|
207
207
|
}
|
|
208
208
|
console.log(section("Next"));
|
|
209
|
+
// FIRST, and before anything that assumes the setup worked. The block above
|
|
210
|
+
// is four manual edits across two files, one of them with a relative path
|
|
211
|
+
// the command itself has to warn about. Without a way to check, the person
|
|
212
|
+
// does not know whether they finished - and the next thing they see is a
|
|
213
|
+
// coverage number that reads 0% when an import is missing.
|
|
214
|
+
console.log(line(`synthesisui doctor confirm the setup above actually took`));
|
|
209
215
|
console.log(line(`synthesisui component ${payload.slug} button bring a component in as YOUR code`));
|
|
210
216
|
console.log(line(`synthesisui template ${payload.slug} landing materialize a whole page`));
|
|
211
217
|
console.log("");
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { syncClaudeMd } from "../claude-md.js";
|
|
4
|
+
import { parseRootTokens } from "../doctor/tokens.js";
|
|
5
|
+
import { body, section, snippet } from "../output.js";
|
|
6
|
+
import { walk } from "./doctor.js";
|
|
7
|
+
/**
|
|
8
|
+
* `synthesisui adopt` - the system somebody ALREADY has, made legible.
|
|
9
|
+
*
|
|
10
|
+
* The whole product assumed you start from nothing. `init` installs OUR tokens
|
|
11
|
+
* and therefore needs four manual edits - import, scope with data-ds, wire
|
|
12
|
+
* fonts - which is where people give up (walked 27/07, the next run reads 0%).
|
|
13
|
+
*
|
|
14
|
+
* The reader who feels our pitch hardest does not need any of that. Their
|
|
15
|
+
* `--acme-*` tokens are already in `:root` and already painting the screen.
|
|
16
|
+
* What is missing is not code, it is the CONTRACT: the file their coding agent
|
|
17
|
+
* reads before writing UI.
|
|
18
|
+
*
|
|
19
|
+
* So this command writes NO CSS, and that is the point rather than a
|
|
20
|
+
* limitation:
|
|
21
|
+
*
|
|
22
|
+
* init writes CSS → needs wiring → 4 edits → 0% if one is wrong
|
|
23
|
+
* adopt writes none → no wiring → works on the first command
|
|
24
|
+
*
|
|
25
|
+
* THE PROMISE, printed rather than buried in docs: it reads your code and
|
|
26
|
+
* rewrites none of it; everything it creates lives in `_synthesisui/` and
|
|
27
|
+
* leaves with `clean`; your tokens keep your names.
|
|
28
|
+
*
|
|
29
|
+
* Dry by default. A command that edits somebody's repository on first run is
|
|
30
|
+
* not a command that people with a system in production ever try.
|
|
31
|
+
*/
|
|
32
|
+
const SCOPE_HINT = ":root, html, :host and @theme";
|
|
33
|
+
/** Group by the prefix an author actually used, which is how they think of it. */
|
|
34
|
+
function byKind(tokens) {
|
|
35
|
+
const kinds = new Map();
|
|
36
|
+
const guess = (n) => /color|colour|bg|background|fg|foreground|surface|canvas|ink|brand/.test(n)
|
|
37
|
+
? "colour"
|
|
38
|
+
: /space|spacing|gap|inset/.test(n)
|
|
39
|
+
? "spacing"
|
|
40
|
+
: /radius|round|corner/.test(n)
|
|
41
|
+
? "radius"
|
|
42
|
+
: /font|type|text|leading|tracking|weight/.test(n)
|
|
43
|
+
? "type"
|
|
44
|
+
: /shadow|elevation/.test(n)
|
|
45
|
+
? "shadow"
|
|
46
|
+
: /duration|ease|motion|transition/.test(n)
|
|
47
|
+
? "motion"
|
|
48
|
+
: "other";
|
|
49
|
+
for (const name of tokens.keys()) {
|
|
50
|
+
const k = guess(name);
|
|
51
|
+
kinds.set(k, (kinds.get(k) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
return [...kinds.entries()].sort((a, b) => b[1] - a[1]);
|
|
54
|
+
}
|
|
55
|
+
/** The commonest `--<prefix>-` in their own names - the slug they already use. */
|
|
56
|
+
function guessSlug(tokens, pkgName) {
|
|
57
|
+
const counts = new Map();
|
|
58
|
+
for (const n of tokens.keys()) {
|
|
59
|
+
const m = /^--([a-z0-9]+)-/.exec(n);
|
|
60
|
+
if (m)
|
|
61
|
+
counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
64
|
+
// Only trust the prefix when it actually dominates; a repo whose tokens are
|
|
65
|
+
// `--color-*`, `--space-*` has no brand prefix and the package name is the
|
|
66
|
+
// better answer.
|
|
67
|
+
if (top &&
|
|
68
|
+
top[1] >= tokens.size * 0.5 &&
|
|
69
|
+
!/^(color|space|font|size)$/.test(top[0])) {
|
|
70
|
+
return top[0];
|
|
71
|
+
}
|
|
72
|
+
return (pkgName
|
|
73
|
+
.replace(/^@[^/]+\//, "")
|
|
74
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
75
|
+
.replace(/^-|-$/g, "")
|
|
76
|
+
.toLowerCase() || "my-system");
|
|
77
|
+
}
|
|
78
|
+
async function findTokens(root, only) {
|
|
79
|
+
let files = 0;
|
|
80
|
+
const from = [];
|
|
81
|
+
const all = new Map();
|
|
82
|
+
const take = async (file) => {
|
|
83
|
+
const css = await readFile(file, "utf8").catch(() => "");
|
|
84
|
+
if (!css)
|
|
85
|
+
return;
|
|
86
|
+
files++;
|
|
87
|
+
const found = parseRootTokens(css);
|
|
88
|
+
if (found.size === 0)
|
|
89
|
+
return;
|
|
90
|
+
from.push(`${relative(root, file)} (${found.size})`);
|
|
91
|
+
for (const [k, v] of found)
|
|
92
|
+
if (!all.has(k))
|
|
93
|
+
all.set(k, v);
|
|
94
|
+
};
|
|
95
|
+
if (only) {
|
|
96
|
+
await take(resolve(root, only));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
for await (const f of walk(root)) {
|
|
100
|
+
if (/\.(css|scss|sass|less)$/i.test(f))
|
|
101
|
+
await take(f);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { tokens: all, files, from };
|
|
105
|
+
}
|
|
106
|
+
/** The contract. Names THEIR tokens - there is no translation layer. */
|
|
107
|
+
function guide(name, slug, tokens) {
|
|
108
|
+
const lines = [...tokens.entries()].map(([k, v]) => ` ${k}: ${v};`);
|
|
109
|
+
return `# ${name} - the contract your agent follows
|
|
110
|
+
|
|
111
|
+
Adopted from this repository by \`synthesisui adopt\`. These are YOUR tokens,
|
|
112
|
+
under YOUR names - nothing was renamed and no stylesheet was rewritten.
|
|
113
|
+
|
|
114
|
+
## The rule
|
|
115
|
+
|
|
116
|
+
When writing or editing UI in this project, use these custom properties.
|
|
117
|
+
Do not write raw colours, spacings or radii that a token already covers.
|
|
118
|
+
|
|
119
|
+
✗ background: #3b82f6
|
|
120
|
+
✓ background: var(--${slug}-color-primary)
|
|
121
|
+
|
|
122
|
+
If a value has no token, say so instead of inventing one silently - a new
|
|
123
|
+
token is a decision for a person to make.
|
|
124
|
+
|
|
125
|
+
## The tokens (${tokens.size})
|
|
126
|
+
|
|
127
|
+
\`\`\`css
|
|
128
|
+
:root {
|
|
129
|
+
${lines.join("\n")}
|
|
130
|
+
}
|
|
131
|
+
\`\`\`
|
|
132
|
+
|
|
133
|
+
## Checking your work
|
|
134
|
+
|
|
135
|
+
npx synthesisui@latest doctor
|
|
136
|
+
|
|
137
|
+
Reports how much of the shipped UI resolves to these tokens, and names the
|
|
138
|
+
token for any hardcoded value that already has one.
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
export async function adopt(opts) {
|
|
142
|
+
const root = resolve(opts.dir ?? process.cwd());
|
|
143
|
+
const pkg = await readFile(join(root, "package.json"), "utf8")
|
|
144
|
+
.then((r) => JSON.parse(r))
|
|
145
|
+
.catch(() => ({ name: undefined }));
|
|
146
|
+
const found = await findTokens(root, opts.tokens);
|
|
147
|
+
console.log(section("Adopt"));
|
|
148
|
+
if (found.tokens.size === 0) {
|
|
149
|
+
// Never "failed to adopt". Always: what was looked for, where, and the
|
|
150
|
+
// next thing to type. Being stuck with no move is the worst outcome.
|
|
151
|
+
console.log(body(`Looked in ${SCOPE_HINT} across ${found.files} stylesheet${found.files === 1 ? "" : "s"} and found no custom properties.`));
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(body("If your tokens live somewhere this did not reach:"));
|
|
154
|
+
console.log(snippet(["npx synthesisui@latest adopt --tokens <path/to.css>"]));
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(body("If you do not have a system yet, start from one:"));
|
|
157
|
+
console.log(body("https://www.synthesisui.com/gallery"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const slug = opts.slug ?? guessSlug(found.tokens, pkg.name ?? "");
|
|
161
|
+
const name = (pkg.name ?? slug).replace(/^@[^/]+\//, "");
|
|
162
|
+
const dir = join(root, "_synthesisui", "ds", slug);
|
|
163
|
+
console.log(body(`Reading your system … ${found.files} stylesheet${found.files === 1 ? "" : "s"}, ${found.tokens.size} tokens found`));
|
|
164
|
+
console.log("");
|
|
165
|
+
for (const [kind, n] of byKind(found.tokens)) {
|
|
166
|
+
console.log(body(` ${kind.padEnd(10)} ${String(n).padStart(3)}`));
|
|
167
|
+
}
|
|
168
|
+
console.log("");
|
|
169
|
+
for (const f of found.from.slice(0, 4))
|
|
170
|
+
console.log(body(` from ${f}`));
|
|
171
|
+
if (found.from.length > 4)
|
|
172
|
+
console.log(body(` and ${found.from.length - 4} more`));
|
|
173
|
+
if (!opts.write) {
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(body("Would create (nothing written yet):"));
|
|
176
|
+
console.log(body(` _synthesisui/ds/${slug}/system.json your system, described`));
|
|
177
|
+
console.log(body(` _synthesisui/ds/${slug}/GUIDE.md what your agent reads`));
|
|
178
|
+
console.log(body(" CLAUDE.md a managed block pointing at it"));
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(body("Your stylesheets are not touched. Nothing outside"));
|
|
181
|
+
console.log(body("_synthesisui/ is written, and `clean` removes all of it."));
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(snippet([
|
|
184
|
+
`npx synthesisui@latest adopt --write${opts.slug ? ` --slug ${slug}` : ""}`,
|
|
185
|
+
]));
|
|
186
|
+
console.log(body(`writes it as "${slug}" - pass --slug to name it differently`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await mkdir(dir, { recursive: true });
|
|
190
|
+
await writeFile(join(dir, "system.json"), `${JSON.stringify({
|
|
191
|
+
meta: { name, slug, adopted: true, source: "repository" },
|
|
192
|
+
tokens: Object.fromEntries(found.tokens),
|
|
193
|
+
}, null, 2)}\n`, "utf8");
|
|
194
|
+
await writeFile(join(dir, "GUIDE.md"), guide(name, slug, found.tokens), "utf8");
|
|
195
|
+
// `version: 0` marks a system nobody published - the doctor and CLAUDE.md
|
|
196
|
+
// read this to know it is adopted rather than installed.
|
|
197
|
+
await writeFile(join(dir, ".lock"), `${JSON.stringify({ slug, name, version: 0, adopted: true }, null, 2)}\n`, "utf8");
|
|
198
|
+
await syncClaudeMd(root);
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log(body(`✓ _synthesisui/ds/${slug}/ written`));
|
|
201
|
+
console.log(body("✓ CLAUDE.md managed block added"));
|
|
202
|
+
console.log("");
|
|
203
|
+
console.log(body("Your agent now knows your system. Nothing of yours changed."));
|
|
204
|
+
console.log("");
|
|
205
|
+
console.log(snippet(["npx synthesisui@latest doctor"]));
|
|
206
|
+
console.log(body("see what it makes measurable"));
|
|
207
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -40,7 +40,7 @@ const SKIP = new Set([
|
|
|
40
40
|
// Our own installed artifacts are the answer, not the problem.
|
|
41
41
|
"_synthesisui",
|
|
42
42
|
]);
|
|
43
|
-
async function* walk(dir) {
|
|
43
|
+
export async function* walk(dir) {
|
|
44
44
|
// `readdir`'s overloads infer a Buffer-named Dirent without an explicit
|
|
45
45
|
// encoding; naming it keeps `e.name` a string.
|
|
46
46
|
const entries = await readdir(dir, {
|
|
@@ -63,7 +63,7 @@ async function* walk(dir) {
|
|
|
63
63
|
}
|
|
64
64
|
/** The same walk over several roots, in the order the caller named them. A
|
|
65
65
|
* single file passed as a scope is read as itself. */
|
|
66
|
-
async function* walkAll(roots) {
|
|
66
|
+
export async function* walkAll(roots) {
|
|
67
67
|
for (const r of roots) {
|
|
68
68
|
if (EXTS.some((x) => r.endsWith(x))) {
|
|
69
69
|
yield r;
|
|
@@ -91,6 +91,7 @@ async function loadSystem(root) {
|
|
|
91
91
|
// reading them all is both correct and what the running app actually sees.
|
|
92
92
|
let css = "";
|
|
93
93
|
let lock = null;
|
|
94
|
+
let adopted = false;
|
|
94
95
|
const recipes = new Map();
|
|
95
96
|
const documents = [];
|
|
96
97
|
for (const slug of slugs) {
|
|
@@ -118,6 +119,24 @@ async function loadSystem(root) {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
css += `\n${real || root}`;
|
|
122
|
+
// An ADOPTED system has no tokens.css of ours - `adopt` deliberately
|
|
123
|
+
// writes no CSS, because the project's own stylesheet already works. Its
|
|
124
|
+
// vocabulary lives in system.json, and without this the doctor falls
|
|
125
|
+
// through to harvesting and then offers to install a system to somebody
|
|
126
|
+
// who just adopted one (caught running it, 27/07).
|
|
127
|
+
if (mine?.adopted) {
|
|
128
|
+
const raw = await readFile(join(dir, "system.json"), "utf8").catch(() => "");
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
for (const [k, v] of Object.entries(parsed.tokens ?? {})) {
|
|
132
|
+
css += `\n:root { ${k}: ${v}; }`;
|
|
133
|
+
}
|
|
134
|
+
adopted = true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Unreadable system.json costs the naming, not the run.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
121
140
|
const docRaw = mine?.version
|
|
122
141
|
? await readFile(join(dir, `v${mine.version}`, "design-system.json"), "utf8").catch(() => "")
|
|
123
142
|
: "";
|
|
@@ -135,7 +154,11 @@ async function loadSystem(root) {
|
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
table: buildTable({ css, lock, source: adopted ? "adopted" : "installed" }),
|
|
159
|
+
recipes,
|
|
160
|
+
documents,
|
|
161
|
+
};
|
|
139
162
|
}
|
|
140
163
|
/**
|
|
141
164
|
* The project's OWN vocabulary, when we did not put one there.
|
|
@@ -257,11 +280,32 @@ export async function doctor(opts) {
|
|
|
257
280
|
const overrides = [];
|
|
258
281
|
const used = new Map();
|
|
259
282
|
const known = new Set(recipes.keys());
|
|
283
|
+
/**
|
|
284
|
+
* INSTALLED IS NOT THE SAME AS WORKING.
|
|
285
|
+
*
|
|
286
|
+
* `init` ends in four manual edits across two files. Skip them - or get the
|
|
287
|
+
* relative path in the import wrong, which the command itself warns about -
|
|
288
|
+
* and the next run reports "Token coverage 0%" with no explanation
|
|
289
|
+
* (reproduced 27/07 on a fresh project). The tool knows the system is
|
|
290
|
+
* installed, because it just read 85 tokens out of it, and reports a number
|
|
291
|
+
* that reads as failure when the truth is an unfinished setup.
|
|
292
|
+
*
|
|
293
|
+
* Nobody debugs from 0%. They conclude the product does not work.
|
|
294
|
+
*/
|
|
295
|
+
const wiring = { imported: false, scoped: false };
|
|
260
296
|
for await (const file of scopes.length > 0 ? walkAll(scopes) : walk(root)) {
|
|
261
297
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
262
298
|
if (!src)
|
|
263
299
|
continue;
|
|
264
300
|
const rel = relative(root, file);
|
|
301
|
+
// The two facts that decide whether an installed system reaches the
|
|
302
|
+
// browser at all. Free: these files are already open and in memory.
|
|
303
|
+
if (table.slug) {
|
|
304
|
+
if (src.includes(`_synthesisui/ds/${table.slug}/tokens.css`))
|
|
305
|
+
wiring.imported = true;
|
|
306
|
+
if (src.includes(`data-ds="${table.slug}"`))
|
|
307
|
+
wiring.scoped = true;
|
|
308
|
+
}
|
|
265
309
|
reports.push(scanSource(rel, src, table));
|
|
266
310
|
if (recipes.size > 0) {
|
|
267
311
|
for (const o of findOverrides(src, recipes))
|
|
@@ -300,9 +344,11 @@ export async function doctor(opts) {
|
|
|
300
344
|
// possible first line.
|
|
301
345
|
table.source === "installed"
|
|
302
346
|
? `${table.name ?? table.slug} v${table.version ?? "?"} - ${plural(table.byName.size, "token")}, ${plural(d.scanned, "file")} read`
|
|
303
|
-
: table.source === "
|
|
304
|
-
?
|
|
305
|
-
:
|
|
347
|
+
: table.source === "adopted"
|
|
348
|
+
? `${table.name ?? table.slug} (adopted) - ${plural(table.byName.size, "token")}, ${plural(d.scanned, "file")} read`
|
|
349
|
+
: table.source === "yours"
|
|
350
|
+
? `Your own tokens - ${plural(table.byName.size, "token")} found, ${plural(d.scanned, "file")} read`
|
|
351
|
+
: `No system installed - ${plural(d.scanned, "file")} read`));
|
|
306
352
|
if (scopes.length > 0) {
|
|
307
353
|
console.log(body(`scope: ${opts.scopes?.join(", ")}`));
|
|
308
354
|
}
|
|
@@ -329,6 +375,25 @@ export async function doctor(opts) {
|
|
|
329
375
|
// 0 of 0 is not a perfect score, it is an empty measurement - printing a
|
|
330
376
|
// full bar there would be the report's first lie.
|
|
331
377
|
const measurable = d.tokenUses + d.findings.length > 0;
|
|
378
|
+
// Before the number, because the number is the thing that misleads. An
|
|
379
|
+
// installed-but-unwired system reads 0%, and 0% reads as "broken product"
|
|
380
|
+
// rather than "one import missing".
|
|
381
|
+
const unwired = table.source === "installed" && (!wiring.imported || !wiring.scoped);
|
|
382
|
+
if (unwired) {
|
|
383
|
+
console.log("");
|
|
384
|
+
console.log(body(`${table.name ?? table.slug} is installed - but not wired up yet.`));
|
|
385
|
+
console.log("");
|
|
386
|
+
console.log(body(wiring.imported
|
|
387
|
+
? ` ✓ some stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`
|
|
388
|
+
: ` ✗ no stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`));
|
|
389
|
+
console.log(body(wiring.scoped
|
|
390
|
+
? ` ✓ data-ds="${table.slug}" found`
|
|
391
|
+
: ` ✗ no element carries data-ds="${table.slug}"`));
|
|
392
|
+
console.log("");
|
|
393
|
+
console.log(body(`Until both are true, none of the ${table.byName.size} tokens reach the browser`));
|
|
394
|
+
console.log(body("and the number below cannot mean anything."));
|
|
395
|
+
console.log(body("The exact snippets are in the output of `init`."));
|
|
396
|
+
}
|
|
332
397
|
if (hasSystem && measurable) {
|
|
333
398
|
console.log("");
|
|
334
399
|
console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
|
|
@@ -661,6 +726,42 @@ export async function doctor(opts) {
|
|
|
661
726
|
}
|
|
662
727
|
console.log("");
|
|
663
728
|
console.log(body("synthesisui doctor --verbose every finding, file by file"));
|
|
729
|
+
/**
|
|
730
|
+
* THE READER WHO GOT A NUMBER AND NOWHERE TO GO.
|
|
731
|
+
*
|
|
732
|
+
* Someone with no system at all is told to start one. Someone running OUR
|
|
733
|
+
* system has commands everywhere. The person in between - who already owns
|
|
734
|
+
* a design system, which is precisely the ICP - got the report and silence,
|
|
735
|
+
* and closed the terminal (walked 27/07).
|
|
736
|
+
*
|
|
737
|
+
* The sentence follows from THEIR number rather than pitching: the value
|
|
738
|
+
* is not that we would name these, it is that their agent has no way to
|
|
739
|
+
* know they should be named. That is also the line worth pasting into a
|
|
740
|
+
* thread, which is the other job this paragraph does.
|
|
741
|
+
*/
|
|
742
|
+
if (table.source === "yours") {
|
|
743
|
+
const named = d.findings.filter((f) => f.token).length;
|
|
744
|
+
const anonymous = d.findings.length - named;
|
|
745
|
+
console.log("");
|
|
746
|
+
console.log(body(named > 0
|
|
747
|
+
? `${named} of these already have a name in your own system.`
|
|
748
|
+
: "None of these have a name in your system yet."));
|
|
749
|
+
if (anonymous > 0) {
|
|
750
|
+
console.log(body(`The other ${anonymous} do not - and your coding agent has no way to know they should.`));
|
|
751
|
+
}
|
|
752
|
+
console.log("");
|
|
753
|
+
// Careful about what is being offered. This reader ALREADY has a design
|
|
754
|
+
// system, so "install ours" would mean replacing theirs, and a CTA that
|
|
755
|
+
// pretends otherwise is the kind of confident overclaim that costs
|
|
756
|
+
// trust the first time somebody follows it. What they are missing is
|
|
757
|
+
// not tokens - it is a contract their agent reads before writing UI.
|
|
758
|
+
console.log(body("Your tokens exist. What your agent is missing is a contract:"));
|
|
759
|
+
console.log(body("rules it reads BEFORE writing UI, and a manifest of what exists."));
|
|
760
|
+
console.log("");
|
|
761
|
+
console.log(snippet(["npx synthesisui@latest init --ds <slug>"]));
|
|
762
|
+
console.log(body("starts you on a system that ships one - browse them at"));
|
|
763
|
+
console.log(body("https://www.synthesisui.com/gallery"));
|
|
764
|
+
}
|
|
664
765
|
console.log("");
|
|
665
766
|
}
|
|
666
767
|
if (opts.strict) {
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -212,7 +212,9 @@ export function parseRootTokens(css) {
|
|
|
212
212
|
}
|
|
213
213
|
export function buildTable(input) {
|
|
214
214
|
const source = input.source ?? "installed";
|
|
215
|
-
const byName = source === "
|
|
215
|
+
const byName = source === "installed"
|
|
216
|
+
? parseTokens(input.css)
|
|
217
|
+
: parseRootTokens(input.css);
|
|
216
218
|
const byValue = new Map();
|
|
217
219
|
for (const [name, value] of byName) {
|
|
218
220
|
const key = normalizeValue(value);
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { add } from "./commands/add.js";
|
|
3
|
+
import { adopt } from "./commands/adopt.js";
|
|
3
4
|
import { advise } from "./commands/advise.js";
|
|
4
5
|
import { clean } from "./commands/clean.js";
|
|
5
6
|
import { component } from "./commands/component.js";
|
|
@@ -123,6 +124,15 @@ async function main() {
|
|
|
123
124
|
const registry = typeof flags.registry === "string" ? flags.registry : undefined;
|
|
124
125
|
const dir = typeof flags.dir === "string" ? flags.dir : undefined;
|
|
125
126
|
switch (command) {
|
|
127
|
+
case "adopt":
|
|
128
|
+
// Dry by default: `--write` is the only way anything lands on disk.
|
|
129
|
+
await adopt({
|
|
130
|
+
dir,
|
|
131
|
+
write: flags.write === true,
|
|
132
|
+
tokens: typeof flags.tokens === "string" ? flags.tokens : undefined,
|
|
133
|
+
slug: typeof flags.slug === "string" ? flags.slug : undefined,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
126
136
|
case "doctor":
|
|
127
137
|
// positional paths scope the READING (the system is still found from the
|
|
128
138
|
// root): `doctor apps/web packages/ui` in a monorepo
|
package/package.json
CHANGED