synthesisui 0.16.33 → 0.16.35
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 +61 -28
- package/dist/commands/import.js +312 -0
- package/dist/doctor/scan.js +28 -0
- package/dist/index.js +13 -0
- package/dist/output.js +25 -1
- package/package.json +2 -2
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/
|
|
|
8
8
|
import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
|
|
9
9
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
10
10
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
11
|
-
import { body, section, snippet } from "../output.js";
|
|
11
|
+
import { body, paint, section, snippet } from "../output.js";
|
|
12
12
|
/**
|
|
13
13
|
* `synthesisui doctor` - the check nobody else ships.
|
|
14
14
|
*
|
|
@@ -43,7 +43,11 @@ const SKIP = new Set([
|
|
|
43
43
|
// Our own installed artifacts are the answer, not the problem.
|
|
44
44
|
"_synthesisui",
|
|
45
45
|
]);
|
|
46
|
-
export async function* walk(dir
|
|
46
|
+
export async function* walk(dir,
|
|
47
|
+
/** Filled with nested projects that were SKIPPED - each speaks its own
|
|
48
|
+
* system and must not be billed against this one (30/07: the monorepo
|
|
49
|
+
* run charged aluna-landing's tokens against the root's harvest). */
|
|
50
|
+
skippedProjects) {
|
|
47
51
|
// `readdir`'s overloads infer a Buffer-named Dirent without an explicit
|
|
48
52
|
// encoding; naming it keeps `e.name` a string.
|
|
49
53
|
const entries = await readdir(dir, {
|
|
@@ -57,7 +61,14 @@ export async function* walk(dir) {
|
|
|
57
61
|
if (e.isDirectory()) {
|
|
58
62
|
if (SKIP.has(e.name))
|
|
59
63
|
continue;
|
|
60
|
-
|
|
64
|
+
// A CHILD directory carrying its own _synthesisui is a separately
|
|
65
|
+
// governed project - its record, its doctor, its numbers.
|
|
66
|
+
const governed = await readdir(join(full, "_synthesisui")).then(() => true, () => false);
|
|
67
|
+
if (governed) {
|
|
68
|
+
skippedProjects?.push(full);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
yield* walk(full, skippedProjects);
|
|
61
72
|
}
|
|
62
73
|
else if (EXTS.some((x) => e.name.endsWith(x))) {
|
|
63
74
|
yield full;
|
|
@@ -199,7 +210,7 @@ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
|
199
210
|
/** A bar you can read at a glance, in a font that is always monospace. */
|
|
200
211
|
function meter(pct, width = 24) {
|
|
201
212
|
const filled = Math.round((pct / 100) * width);
|
|
202
|
-
return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
213
|
+
return `${paint.blue("█".repeat(filled))}${paint.faint("░".repeat(width - filled))}`;
|
|
203
214
|
}
|
|
204
215
|
function verdict(d, hasSystem, overruled, conflicts) {
|
|
205
216
|
// A system at war with itself outranks every other number here. The closing
|
|
@@ -368,7 +379,10 @@ export async function doctor(opts) {
|
|
|
368
379
|
* Nobody debugs from 0%. They conclude the product does not work.
|
|
369
380
|
*/
|
|
370
381
|
const wiring = await readWiring(root, table.slug);
|
|
371
|
-
|
|
382
|
+
const skippedProjects = [];
|
|
383
|
+
for await (const file of scopes.length > 0
|
|
384
|
+
? walkAll(scopes)
|
|
385
|
+
: walk(root, skippedProjects)) {
|
|
372
386
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
373
387
|
if (!src)
|
|
374
388
|
continue;
|
|
@@ -442,7 +456,11 @@ export async function doctor(opts) {
|
|
|
442
456
|
// `var(--ds-color-semantic-knob, #ffffff)` - is set aside for the opposite
|
|
443
457
|
// reason: it is already tokenized. The summary said the false half out
|
|
444
458
|
// loud and hid the true half behind a flag.
|
|
445
|
-
console.log(body(`set aside ${plural(asideTotal, "value")} that are not drift
|
|
459
|
+
console.log(body(`set aside ${plural(asideTotal, "value")} that are not drift` +
|
|
460
|
+
(skippedProjects.length > 0
|
|
461
|
+
? ` and ${plural(skippedProjects.length, "nested project")} that speak their own systems`
|
|
462
|
+
: "") +
|
|
463
|
+
" (--verbose for why)"));
|
|
446
464
|
}
|
|
447
465
|
// 0 of 0 is not a perfect score, it is an empty measurement - printing a
|
|
448
466
|
// full bar there would be the report's first lie.
|
|
@@ -522,8 +540,19 @@ export async function doctor(opts) {
|
|
|
522
540
|
}
|
|
523
541
|
if (hasSystem && measurable) {
|
|
524
542
|
console.log("");
|
|
525
|
-
console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
|
|
526
|
-
console.log(body(` ${d.tokenUses} from the system, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`));
|
|
543
|
+
console.log(body(`Token coverage ${meter(d.coverage)} ${paint.strong(`${String(d.coverage).padStart(3)}%`)}`));
|
|
544
|
+
console.log(body(paint.dim(` ${d.tokenUses} from the system, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`)));
|
|
545
|
+
// The strongest number leads, not trails: it used to sit two screens
|
|
546
|
+
// down, after the phantom list (30/07). For the reader who already owns
|
|
547
|
+
// a system - the ICP - THIS line is the report.
|
|
548
|
+
if (table.source === "yours" && d.findings.length > 0) {
|
|
549
|
+
const named = d.findings.filter((f) => f.token).length;
|
|
550
|
+
if (named > 0) {
|
|
551
|
+
console.log("");
|
|
552
|
+
console.log(body(`${paint.strong(String(named))} of the ${d.findings.length} hand-written values already have a name in YOUR system.`));
|
|
553
|
+
console.log(body(paint.dim("Your agent has no way to know: the tokens exist, the contract does not.")));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
527
556
|
}
|
|
528
557
|
/**
|
|
529
558
|
* THE RECORD - the layer that remembers, printed where the moment is shown.
|
|
@@ -635,13 +664,21 @@ export async function doctor(opts) {
|
|
|
635
664
|
console.log(body(`${plural(phantoms.size, "name")} written as tokens that your system never declares.`));
|
|
636
665
|
console.log(body("An undeclared custom property applies nothing at all."));
|
|
637
666
|
console.log("");
|
|
638
|
-
|
|
667
|
+
// Capped like every other section: 99 lines is a report nobody reads,
|
|
668
|
+
// and it buried the strongest number two screens down (30/07). The full
|
|
669
|
+
// list keeps living behind --verbose.
|
|
670
|
+
const PHANTOM_CAP = 8;
|
|
671
|
+
const sorted = [...phantoms].sort();
|
|
672
|
+
for (const [name, at] of sorted.slice(0, PHANTOM_CAP)) {
|
|
639
673
|
const where = [...at.files][0];
|
|
640
|
-
console.log(body(`${name} ${where}:${at.line}${at.files.size > 1 ? ` +${at.files.size - 1} more` : ""}`));
|
|
674
|
+
console.log(body(`${name} ${paint.dim(`${where}:${at.line}${at.files.size > 1 ? ` +${at.files.size - 1} more` : ""}`)}`));
|
|
641
675
|
const kin = siblingTokens(name, table);
|
|
642
676
|
if (kin.length > 0)
|
|
643
677
|
console.log(snippet([`your system has ${kin.join(", ")}`]));
|
|
644
678
|
}
|
|
679
|
+
if (sorted.length > PHANTOM_CAP) {
|
|
680
|
+
console.log(body(paint.dim(`and ${sorted.length - PHANTOM_CAP} more · synthesisui doctor --verbose`)));
|
|
681
|
+
}
|
|
645
682
|
console.log("");
|
|
646
683
|
console.log(body("Add the name to the system, or use one it has. Do not leave it."));
|
|
647
684
|
}
|
|
@@ -1009,11 +1046,16 @@ export async function doctor(opts) {
|
|
|
1009
1046
|
}
|
|
1010
1047
|
if (d.findings.length > 0 || overrides.length > 0) {
|
|
1011
1048
|
console.log("");
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1049
|
+
if (table.source !== "yours") {
|
|
1050
|
+
console.log(body(`${plural(d.findings.length, "value")} by hand · ${d.named} already ${d.named === 1 ? "has" : "have"} a name`));
|
|
1051
|
+
}
|
|
1052
|
+
// An all-zero status line is filler, not information.
|
|
1053
|
+
if (overrides.length > 0 || offSystemCount > 0) {
|
|
1054
|
+
console.log(body(`${plural(overrides.length - lawKeepingCount, "override")} · ${offSystemCount} left the system` +
|
|
1055
|
+
(lawKeepingCount > 0
|
|
1056
|
+
? ` · ${lawKeepingCount} more kept a law`
|
|
1057
|
+
: "")));
|
|
1058
|
+
}
|
|
1017
1059
|
}
|
|
1018
1060
|
if (plan.length > 0) {
|
|
1019
1061
|
console.log("");
|
|
@@ -1021,7 +1063,7 @@ export async function doctor(opts) {
|
|
|
1021
1063
|
const w = Math.min(52, Math.max(...plan.slice(0, 5).map((j) => j.what.length)));
|
|
1022
1064
|
plan.slice(0, 5).forEach((j, i) => {
|
|
1023
1065
|
const what = j.what.length > w ? `${j.what.slice(0, w - 1)}…` : j.what.padEnd(w);
|
|
1024
|
-
console.log(` ${i + 1}. ${what} ${j.size}${j.cheap ? " (cheap)" : ""}`);
|
|
1066
|
+
console.log(` ${i + 1}. ${what} ${paint.dim(`${j.size}${j.cheap ? " (cheap)" : ""}`)}`);
|
|
1025
1067
|
});
|
|
1026
1068
|
}
|
|
1027
1069
|
console.log("");
|
|
@@ -1040,15 +1082,6 @@ export async function doctor(opts) {
|
|
|
1040
1082
|
* thread, which is the other job this paragraph does.
|
|
1041
1083
|
*/
|
|
1042
1084
|
if (table.source === "yours") {
|
|
1043
|
-
const named = d.findings.filter((f) => f.token).length;
|
|
1044
|
-
const anonymous = d.findings.length - named;
|
|
1045
|
-
console.log("");
|
|
1046
|
-
console.log(body(named > 0
|
|
1047
|
-
? `${named} of these already have a name in your own system.`
|
|
1048
|
-
: "None of these have a name in your system yet."));
|
|
1049
|
-
if (anonymous > 0) {
|
|
1050
|
-
console.log(body(`The other ${anonymous} do not - and your coding agent has no way to know they should.`));
|
|
1051
|
-
}
|
|
1052
1085
|
console.log("");
|
|
1053
1086
|
// Careful about what is being offered. This reader ALREADY has a design
|
|
1054
1087
|
// system, so "install ours" would mean replacing theirs, and a CTA that
|
|
@@ -1058,9 +1091,9 @@ export async function doctor(opts) {
|
|
|
1058
1091
|
console.log(body("Your tokens exist. What your agent is missing is a contract:"));
|
|
1059
1092
|
console.log(body("rules it reads BEFORE writing UI, and a manifest of what exists."));
|
|
1060
1093
|
console.log("");
|
|
1061
|
-
console.log(snippet(["npx synthesisui@latest init --ds <slug>"]));
|
|
1062
|
-
console.log(body("starts you on a system that ships one - browse them at"));
|
|
1063
|
-
console.log(body("https://www.synthesisui.com/gallery"));
|
|
1094
|
+
console.log(paint.blue(snippet(["npx synthesisui@latest init --ds <slug>"])));
|
|
1095
|
+
console.log(body(paint.dim("starts you on a system that ships one - browse them at")));
|
|
1096
|
+
console.log(body(paint.blue("https://www.synthesisui.com/gallery")));
|
|
1064
1097
|
}
|
|
1065
1098
|
console.log("");
|
|
1066
1099
|
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { readToken, resolveRegistry } from "../config.js";
|
|
4
|
+
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
5
|
+
import { buildTable } from "../doctor/tokens.js";
|
|
6
|
+
import { body, paint, section } from "../output.js";
|
|
7
|
+
import { walk, walkAll } from "./doctor.js";
|
|
8
|
+
/** How many distinct values travel. A census is a vocabulary, not a log: past
|
|
9
|
+
* the first few dozen repeats the tail is noise nobody will name. */
|
|
10
|
+
const MAX_VALUES = 120;
|
|
11
|
+
/**
|
|
12
|
+
* Dependencies as the project actually resolves them - which means reading
|
|
13
|
+
* ANCESTOR package.json files too.
|
|
14
|
+
*
|
|
15
|
+
* Measured on our own repo: `import --dir apps/web` reported "plain css" for a
|
|
16
|
+
* Next + React + Tailwind app, because a workspace hoists those to the root and
|
|
17
|
+
* the leaf package.json lists only its own icons. Three levels up covers every
|
|
18
|
+
* pnpm/npm workspace layout without wandering into someone's home directory.
|
|
19
|
+
*/
|
|
20
|
+
async function resolveDeps(root) {
|
|
21
|
+
const deps = {};
|
|
22
|
+
let dir = root;
|
|
23
|
+
for (let up = 0; up < 4; up++) {
|
|
24
|
+
const raw = await readFile(join(dir, "package.json"), "utf8").catch(() => null);
|
|
25
|
+
if (raw) {
|
|
26
|
+
try {
|
|
27
|
+
const p = JSON.parse(raw);
|
|
28
|
+
// The nearest package.json wins on a version clash; we only ever ask
|
|
29
|
+
// whether a name is present, so first-seen is enough.
|
|
30
|
+
for (const [k, v] of Object.entries({
|
|
31
|
+
...p.dependencies,
|
|
32
|
+
...p.devDependencies,
|
|
33
|
+
})) {
|
|
34
|
+
if (deps[k] == null)
|
|
35
|
+
deps[k] = String(v);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// unreadable manifest costs the detection, not the run
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const parent = join(dir, "..");
|
|
43
|
+
if (parent === dir)
|
|
44
|
+
break;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
return deps;
|
|
48
|
+
}
|
|
49
|
+
async function detectStack(root) {
|
|
50
|
+
const stack = [];
|
|
51
|
+
const has = async (f) => (await readFile(join(root, f), "utf8").catch(() => null)) !== null;
|
|
52
|
+
const deps = await resolveDeps(root);
|
|
53
|
+
if (await has("components.json"))
|
|
54
|
+
stack.push("shadcn/ui");
|
|
55
|
+
if (deps.next)
|
|
56
|
+
stack.push("next");
|
|
57
|
+
else if (deps.vite)
|
|
58
|
+
stack.push("vite");
|
|
59
|
+
if (deps.react)
|
|
60
|
+
stack.push("react");
|
|
61
|
+
else if (deps.vue)
|
|
62
|
+
stack.push("vue");
|
|
63
|
+
else if (deps.svelte)
|
|
64
|
+
stack.push("svelte");
|
|
65
|
+
if (deps.tailwindcss)
|
|
66
|
+
stack.push("tailwind");
|
|
67
|
+
if (await has("tokens.json"))
|
|
68
|
+
stack.push("tokens.json");
|
|
69
|
+
// Nothing recognised is itself a finding: plain CSS is a supported entrance,
|
|
70
|
+
// and saying so beats an empty list that reads like a failed detection.
|
|
71
|
+
if (stack.length === 0)
|
|
72
|
+
stack.push("plain css");
|
|
73
|
+
return stack;
|
|
74
|
+
}
|
|
75
|
+
/** Their own vocabulary, read from stylesheets - the same harvest the doctor
|
|
76
|
+
* runs when nothing of ours is installed. */
|
|
77
|
+
async function declaredTokens(root) {
|
|
78
|
+
let css = "";
|
|
79
|
+
for await (const file of walkAll([root])) {
|
|
80
|
+
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
81
|
+
continue;
|
|
82
|
+
css += `\n${await readFile(file, "utf8").catch(() => "")}`;
|
|
83
|
+
}
|
|
84
|
+
const table = buildTable({ css, source: "yours" });
|
|
85
|
+
return Object.fromEntries(table.byName);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* EVERY distinct design value, commonest first - not just the repeated ones.
|
|
89
|
+
*
|
|
90
|
+
* The doctor's `repeats` list deliberately drops single-use values, because as a
|
|
91
|
+
* to-do list "this appears once" is noise. A CENSUS is the opposite kind of
|
|
92
|
+
* document: it is a vocabulary, and a brand colour that lives in one reusable
|
|
93
|
+
* button is still the brand colour. Caught on a fake v0 app whose primary
|
|
94
|
+
* (#7c3aed) appeared exactly once and vanished from the payload.
|
|
95
|
+
*/
|
|
96
|
+
function distinctValues(d) {
|
|
97
|
+
const by = new Map();
|
|
98
|
+
for (const f of d.findings) {
|
|
99
|
+
const key = `${f.kind}:${f.literal.toLowerCase()}`;
|
|
100
|
+
const hit = by.get(key);
|
|
101
|
+
if (hit) {
|
|
102
|
+
hit.count += 1;
|
|
103
|
+
hit.files.add(f.file);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
by.set(key, {
|
|
107
|
+
kind: f.kind,
|
|
108
|
+
value: f.literal,
|
|
109
|
+
count: 1,
|
|
110
|
+
files: new Set([f.file]),
|
|
111
|
+
token: f.token,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return [...by.values()]
|
|
115
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value))
|
|
116
|
+
.slice(0, MAX_VALUES)
|
|
117
|
+
.map((v) => ({
|
|
118
|
+
kind: v.kind,
|
|
119
|
+
value: v.value,
|
|
120
|
+
count: v.count,
|
|
121
|
+
files: v.files.size,
|
|
122
|
+
token: v.token,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
export async function takeCensus(root) {
|
|
126
|
+
const table = buildTable({ css: "", source: "yours" });
|
|
127
|
+
const reports = [];
|
|
128
|
+
for await (const file of walk(root)) {
|
|
129
|
+
const src = await readFile(file, "utf8").catch(() => "");
|
|
130
|
+
if (!src)
|
|
131
|
+
continue;
|
|
132
|
+
reports.push(scanSource(relative(root, file), src, table));
|
|
133
|
+
}
|
|
134
|
+
const d = diagnose(reports);
|
|
135
|
+
const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
136
|
+
let name = null;
|
|
137
|
+
if (pkgRaw) {
|
|
138
|
+
try {
|
|
139
|
+
name = JSON.parse(pkgRaw).name ?? null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// an unreadable package.json costs the name, not the run
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
census: 1,
|
|
147
|
+
project: { name, stack: await detectStack(root) },
|
|
148
|
+
declared: await declaredTokens(root),
|
|
149
|
+
observed: distinctValues(d),
|
|
150
|
+
totals: {
|
|
151
|
+
scanned: d.scanned,
|
|
152
|
+
values: d.findings.length,
|
|
153
|
+
named: d.named,
|
|
154
|
+
tokenUses: d.tokenUses,
|
|
155
|
+
coverage: d.coverage,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function summarize(c) {
|
|
160
|
+
const byKind = new Map();
|
|
161
|
+
for (const v of c.observed)
|
|
162
|
+
byKind.set(v.kind, (byKind.get(v.kind) ?? 0) + v.count);
|
|
163
|
+
const kinds = [...byKind.entries()]
|
|
164
|
+
.sort((a, b) => b[1] - a[1])
|
|
165
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
166
|
+
.join(" · ");
|
|
167
|
+
console.log(section("What your project says"));
|
|
168
|
+
console.log(body(`${c.project.stack.join(" · ")}`));
|
|
169
|
+
console.log(body(`${c.totals.scanned} files · ${c.observed.length} distinct design values`));
|
|
170
|
+
if (kinds)
|
|
171
|
+
console.log(body(kinds));
|
|
172
|
+
if (Object.keys(c.declared).length > 0) {
|
|
173
|
+
console.log(body(`${Object.keys(c.declared).length} tokens you already declare - your names travel unchanged`));
|
|
174
|
+
}
|
|
175
|
+
const top = c.observed.slice(0, 5);
|
|
176
|
+
if (top.length > 0) {
|
|
177
|
+
console.log("");
|
|
178
|
+
for (const v of top) {
|
|
179
|
+
console.log(body(`${paint.strong(v.value)} ${paint.faint(`${v.kind}, ${v.count}× in ${v.files} file${v.files === 1 ? "" : "s"}`)}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The contract an agent needs, printed where an agent will read it.
|
|
185
|
+
*
|
|
186
|
+
* A skill file cannot help the FIRST import - it is not installed yet, and this
|
|
187
|
+
* is the moment before any relationship exists. Terminal output, though, is
|
|
188
|
+
* already in the agent's context: it ran the command. So the command teaches.
|
|
189
|
+
*
|
|
190
|
+
* What we ask for is strictly JUDGEMENT, never numbers. Which of three greys is
|
|
191
|
+
* the real grey, which family is the display voice, what this product is. The
|
|
192
|
+
* census stays arithmetic, and every value in `roles` must be one the census
|
|
193
|
+
* already found - the endpoint drops anything else.
|
|
194
|
+
*/
|
|
195
|
+
function printAgentContract() {
|
|
196
|
+
console.log("");
|
|
197
|
+
console.log(section("If you are an agent reading this"));
|
|
198
|
+
console.log(body("The census is deterministic and complete. What it cannot do is judge, so"));
|
|
199
|
+
console.log(body("add a `reading` object to the file and send that back to us:"));
|
|
200
|
+
console.log("");
|
|
201
|
+
console.log(body(paint.faint(' "reading": {')));
|
|
202
|
+
console.log(body(paint.faint(' "roles": { "canvas": "#…", "foreground": "#…", "primary": "#…" },')));
|
|
203
|
+
console.log(body(paint.faint(' "fonts": { "display": "…", "body": "…" },')));
|
|
204
|
+
console.log(body(paint.faint(' "concept": "one paragraph on what this product is"')));
|
|
205
|
+
console.log(body(paint.faint(" }")));
|
|
206
|
+
console.log("");
|
|
207
|
+
console.log(body("Every hex in `roles` must be one the census already observed - a value"));
|
|
208
|
+
console.log(body("we did not find in their code is dropped. Then send the file:"));
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(body(` ${paint.strong("synthesisui import --census _synthesisui/census.json")}`));
|
|
211
|
+
}
|
|
212
|
+
export async function runImport(opts) {
|
|
213
|
+
const root = opts.root ?? process.cwd();
|
|
214
|
+
// A census handed to us (an agent annotated it) is sent as-is; the numbers
|
|
215
|
+
// inside were still ours to begin with.
|
|
216
|
+
let census;
|
|
217
|
+
if (opts.census) {
|
|
218
|
+
const raw = await readFile(opts.census, "utf8").catch(() => null);
|
|
219
|
+
if (!raw) {
|
|
220
|
+
console.log(section("Import"));
|
|
221
|
+
console.log(body(`Cannot read ${opts.census}.`));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
census = JSON.parse(raw);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
console.log(section("Import"));
|
|
229
|
+
console.log(body(`${opts.census} is not valid JSON.`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (census.census !== 1) {
|
|
233
|
+
console.log(section("Import"));
|
|
234
|
+
console.log(body("That file is not a census this version can send."));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(section("Reading your project"));
|
|
240
|
+
census = await takeCensus(root);
|
|
241
|
+
}
|
|
242
|
+
summarize(census);
|
|
243
|
+
const out = join(root, "_synthesisui", "census.json");
|
|
244
|
+
await mkdir(join(root, "_synthesisui"), { recursive: true });
|
|
245
|
+
await writeFile(out, `${JSON.stringify(census, null, 2)}\n`, "utf8");
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log(body(`Written to ${paint.strong(relative(root, out))}`));
|
|
248
|
+
if (opts.dry) {
|
|
249
|
+
console.log(body("Nothing was sent. Read the file, then run it without --dry."));
|
|
250
|
+
printAgentContract();
|
|
251
|
+
console.log("");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const token = await readToken();
|
|
255
|
+
if (!token) {
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log(body("To turn this into a system on your account:"));
|
|
258
|
+
console.log(body(` ${paint.strong("synthesisui login")}`));
|
|
259
|
+
console.log(body(" synthesisui import"));
|
|
260
|
+
console.log("");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const base = resolveRegistry(opts.registry);
|
|
264
|
+
const res = await fetch(`${base}/api/onboarding/import`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
Authorization: `Bearer ${token}`,
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({ census, name: opts.name }),
|
|
271
|
+
}).catch(() => null);
|
|
272
|
+
if (!res || !res.ok) {
|
|
273
|
+
const detail = res
|
|
274
|
+
? await res
|
|
275
|
+
.json()
|
|
276
|
+
.then((b) => b?.message ?? null)
|
|
277
|
+
.catch(() => null)
|
|
278
|
+
: null;
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log(body(res
|
|
281
|
+
? `The registry refused it (HTTP ${res.status})${detail ? `: ${detail}` : "."}`
|
|
282
|
+
: "Could not reach the registry. The census is on disk - try again later."));
|
|
283
|
+
console.log("");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const payload = (await res.json().catch(() => null));
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(section("Your system exists"));
|
|
289
|
+
console.log(body(`${paint.strong(payload?.name ?? "Your system")} - v1 mirrors your tokens exactly, nothing improved yet.`));
|
|
290
|
+
for (const note of payload?.notes ?? [])
|
|
291
|
+
console.log(body(paint.dim(note)));
|
|
292
|
+
// What a v2 would be worth, in their own values. Said here because this is
|
|
293
|
+
// where the person is standing, and because a number they can check beats an
|
|
294
|
+
// invitation they have to trust.
|
|
295
|
+
const n = payload?.normalize;
|
|
296
|
+
if (n?.decisions && n.decisions > 0) {
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log(section("What a v2 would normalize"));
|
|
299
|
+
console.log(body(`${paint.strong(String(n.decisions))} colour${n.decisions === 1 ? "" : "s"} in your code ${n.decisions === 1 ? "is" : "are"} the same decision typed twice - ${paint.strong(String(n.retires ?? 0))} uses would retire.`));
|
|
300
|
+
for (const line of n.lines ?? [])
|
|
301
|
+
console.log(body(paint.dim(line)));
|
|
302
|
+
console.log("");
|
|
303
|
+
console.log(body("Nothing was changed. You approve it there:"));
|
|
304
|
+
}
|
|
305
|
+
else if (payload?.url) {
|
|
306
|
+
console.log("");
|
|
307
|
+
console.log(body("Name it, see it, and govern it:"));
|
|
308
|
+
}
|
|
309
|
+
if (payload?.url)
|
|
310
|
+
console.log(body(` ${paint.blue(payload.url)}`));
|
|
311
|
+
console.log("");
|
|
312
|
+
}
|
package/dist/doctor/scan.js
CHANGED
|
@@ -231,7 +231,35 @@ function fallbackSpans(line, table) {
|
|
|
231
231
|
const DECLARES_TOKEN = /^\s*(--[a-z0-9_-]+)\s*:/i;
|
|
232
232
|
/** Under this, a radius or spacing value is idiom rather than a decision. */
|
|
233
233
|
const IDIOM = new Set(["0", "0px", "1px", "9999px", "100%", "50%"]);
|
|
234
|
+
/** Test fixtures speak in example names on purpose - `--ds-nope` inside a
|
|
235
|
+
* spec is the TEST, not drift. Every real project has these; accusing them
|
|
236
|
+
* buried the report's real findings under 99 phantom lines the first time
|
|
237
|
+
* the doctor ran on a repo with its own test suite (30/07). */
|
|
238
|
+
const IS_FIXTURE = /(\.(spec|test)\.[a-z]+$|__tests__\/|\/fixtures?\/)/;
|
|
234
239
|
export function scanSource(file, source, table) {
|
|
240
|
+
if (IS_FIXTURE.test(file)) {
|
|
241
|
+
const real = scanCore(file, source, table);
|
|
242
|
+
const aside = real.findings.length +
|
|
243
|
+
(real.phantoms?.length ?? 0) +
|
|
244
|
+
(real.setAside?.reduce((n, a) => n + a.count, 0) ?? 0);
|
|
245
|
+
return {
|
|
246
|
+
file,
|
|
247
|
+
findings: [],
|
|
248
|
+
// Neither accused nor credited: fixture var() uses would inflate
|
|
249
|
+
// coverage exactly as unfairly as fixture hexes would deflate it.
|
|
250
|
+
tokenUses: 0,
|
|
251
|
+
...(aside > 0
|
|
252
|
+
? {
|
|
253
|
+
setAside: [
|
|
254
|
+
{ reason: "test fixtures speak in example names", count: aside },
|
|
255
|
+
],
|
|
256
|
+
}
|
|
257
|
+
: null),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return scanCore(file, source, table);
|
|
261
|
+
}
|
|
262
|
+
function scanCore(file, source, table) {
|
|
235
263
|
const findings = [];
|
|
236
264
|
const phantoms = [];
|
|
237
265
|
let tokenUses = 0;
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { connect } from "./commands/connect.js";
|
|
|
9
9
|
import { doctor } from "./commands/doctor.js";
|
|
10
10
|
import { generate } from "./commands/generate.js";
|
|
11
11
|
import { hook } from "./commands/hook.js";
|
|
12
|
+
import { runImport } from "./commands/import.js";
|
|
12
13
|
import { init } from "./commands/init.js";
|
|
13
14
|
import { list } from "./commands/list.js";
|
|
14
15
|
import { login } from "./commands/login.js";
|
|
@@ -40,6 +41,7 @@ Usage - deterministic, FREE:
|
|
|
40
41
|
|
|
41
42
|
Usage - governance (deterministic, FREE):
|
|
42
43
|
synthesisui adopt [--write] turn the design system you ALREADY have into a contract
|
|
44
|
+
synthesisui import [--dry] read the app you already have and make it a system
|
|
43
45
|
your agent follows - without touching your CSS
|
|
44
46
|
synthesisui connect wire your agent: the check as an editor hook, the system
|
|
45
47
|
as MCP tools, and a contract that stops repeating itself
|
|
@@ -86,6 +88,7 @@ Options:
|
|
|
86
88
|
|
|
87
89
|
Examples:
|
|
88
90
|
synthesisui adopt # you already have a system: start here
|
|
91
|
+
synthesisui import --dry # see the census before anything is sent
|
|
89
92
|
synthesisui login
|
|
90
93
|
synthesisui init --target next
|
|
91
94
|
synthesisui init --target next --ds halogen bootstrap + bring a system in
|
|
@@ -149,6 +152,16 @@ async function main() {
|
|
|
149
152
|
const registry = typeof flags.registry === "string" ? flags.registry : undefined;
|
|
150
153
|
const dir = typeof flags.dir === "string" ? flags.dir : undefined;
|
|
151
154
|
switch (command) {
|
|
155
|
+
case "import":
|
|
156
|
+
// The census is arithmetic over their files; --dry keeps it on disk.
|
|
157
|
+
await runImport({
|
|
158
|
+
root: dir,
|
|
159
|
+
dry: flags.dry === true,
|
|
160
|
+
census: typeof flags.census === "string" ? flags.census : undefined,
|
|
161
|
+
name: typeof flags.name === "string" ? flags.name : undefined,
|
|
162
|
+
registry,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
152
165
|
case "adopt":
|
|
153
166
|
// Dry by default: `--write` is the only way anything lands on disk.
|
|
154
167
|
await adopt({
|
package/dist/output.js
CHANGED
|
@@ -10,7 +10,7 @@ const WIDTH = 66;
|
|
|
10
10
|
export function section(title) {
|
|
11
11
|
const head = `── ${title} `;
|
|
12
12
|
const rest = Math.max(4, WIDTH - head.length);
|
|
13
|
-
return `\n${head}${"─".repeat(rest)}\n`;
|
|
13
|
+
return `\n${head}${paint.faint("─".repeat(rest))}\n`;
|
|
14
14
|
}
|
|
15
15
|
/** Indented, copy-pasteable code block (empty lines stay truly empty). */
|
|
16
16
|
export function snippet(lines) {
|
|
@@ -20,3 +20,27 @@ export function snippet(lines) {
|
|
|
20
20
|
export function body(line) {
|
|
21
21
|
return ` ${line}`;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Colour, used the way the product uses motion: a quiet base and a few
|
|
25
|
+
* deliberate accents (dono, 30/07: "um cinza mais escuro e um mais claro,
|
|
26
|
+
* e o coverage com um azul bem discreto"). Two grays carry hierarchy -
|
|
27
|
+
* chrome/meta in the darker one, secondary prose in the lighter - and one
|
|
28
|
+
* muted steel blue marks what is ALIVE: the coverage bar, commands, links.
|
|
29
|
+
*
|
|
30
|
+
* Off out of respect wherever colour is not wanted: piped output (no TTY),
|
|
31
|
+
* NO_COLOR, or a dumb terminal - assertions in tests never see a code.
|
|
32
|
+
*/
|
|
33
|
+
const COLOR_ON = Boolean(process.stdout.isTTY) &&
|
|
34
|
+
!process.env.NO_COLOR &&
|
|
35
|
+
process.env.TERM !== "dumb";
|
|
36
|
+
const wrap = (code) => (s) => COLOR_ON ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
37
|
+
export const paint = {
|
|
38
|
+
/** Lighter gray - secondary prose, explanations, counts. */
|
|
39
|
+
dim: wrap("38;5;246"),
|
|
40
|
+
/** Darker gray - chrome: rules, meters' empty track, set-aside notes. */
|
|
41
|
+
faint: wrap("38;5;240"),
|
|
42
|
+
/** Discreet steel blue - the coverage fill, commands, links. */
|
|
43
|
+
blue: wrap("38;5;67"),
|
|
44
|
+
/** Bold - the one or two numbers that carry the report. */
|
|
45
|
+
strong: wrap("1"),
|
|
46
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synthesisui",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.35",
|
|
4
4
|
"description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
33
33
|
"dev": "tsx src/index.ts",
|
|
34
34
|
"prepublishOnly": "npm run build",
|
|
35
35
|
"test": "vitest run"
|