synthesisui 0.16.77 → 0.16.79
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/anatomy-read.js +272 -0
- package/dist/commands/add.js +23 -1
- package/dist/commands/component.js +48 -23
- package/dist/commands/doctor.js +49 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/import.js +90 -15
- package/dist/commands/mcp.js +119 -0
- package/dist/commands/refit.js +2 -1
- package/dist/commands/upgrade.js +6 -1
- package/dist/component-codegen.js +281 -37
- package/dist/doctor/dependencies.js +90 -0
- package/dist/doctor/transcribe.js +117 -0
- package/dist/project-facts.js +145 -0
- package/dist/skill-import.js +253 -36
- package/package.json +1 -1
package/dist/commands/import.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
3
4
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
4
5
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
5
6
|
import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
|
|
@@ -11,9 +12,9 @@ import { describeConvention, describeRemainder, detectConventions, } from "../do
|
|
|
11
12
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
12
13
|
import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
|
|
13
14
|
import { buildTable } from "../doctor/tokens.js";
|
|
14
|
-
import { rootClasses, rootTag, transcribe,
|
|
15
|
+
import { rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
|
|
15
16
|
import { body, paint, section } from "../output.js";
|
|
16
|
-
import { detectStack } from "../stack.js";
|
|
17
|
+
import { detectStack, resolveDeps } from "../stack.js";
|
|
17
18
|
import { walk, walkAll } from "./doctor.js";
|
|
18
19
|
/**
|
|
19
20
|
* How many distinct values travel, PER KIND.
|
|
@@ -896,28 +897,79 @@ function sayReach(c) {
|
|
|
896
897
|
}
|
|
897
898
|
}
|
|
898
899
|
/**
|
|
899
|
-
* Fold the skill's
|
|
900
|
+
* Fold the skill's anatomy into the transcription, in place.
|
|
901
|
+
*
|
|
902
|
+
* A CLEAN SPLIT OF WHO KNOWS WHAT. Only the skill can say that a span holds the
|
|
903
|
+
* title and that the thing in the middle is their own `TextEditor`; only this
|
|
904
|
+
* side can say that `text-ocean-500` is `{color.ocean.500}` and that
|
|
905
|
+
* `@tiptap/react` is pinned at `^2.1.0` in their manifest. The platform then
|
|
906
|
+
* turns declarations into roles, because only it has the two palettes.
|
|
900
907
|
*
|
|
901
908
|
* Their declared tokens come from the census itself, so a run that only sends a
|
|
902
909
|
* file it was handed still resolves a `bg-ocean-500` by their own name.
|
|
903
910
|
*/
|
|
904
|
-
function resolveReadParts(census) {
|
|
911
|
+
async function resolveReadParts(census, root) {
|
|
905
912
|
const read = census.reading?.components;
|
|
906
913
|
if (!read)
|
|
907
914
|
return;
|
|
908
915
|
const declared = new Map(Object.entries(census.declared));
|
|
909
|
-
|
|
916
|
+
// The manifest is read ONCE for the whole run: the version belongs in a rule's
|
|
917
|
+
// EVIDENCE, never in its text, so the rule does not go stale on an upgrade and
|
|
918
|
+
// the number is not lost either.
|
|
919
|
+
const deps = await resolveDeps(root).catch(() => ({}));
|
|
920
|
+
census.looks ??= {};
|
|
921
|
+
const looks = census.looks;
|
|
910
922
|
let named = 0;
|
|
923
|
+
let shaped = 0;
|
|
924
|
+
let edges = 0;
|
|
925
|
+
const libs = new Set();
|
|
926
|
+
const notes = new Set();
|
|
927
|
+
/**
|
|
928
|
+
* WHAT A NAME IN THEIR CODE REACHES IN THE SYSTEM - from the crosswalk.
|
|
929
|
+
*
|
|
930
|
+
* The census already decided this for every component it read: `Tag` is
|
|
931
|
+
* `nearly`, canonical `badge`; `ToolbarButton` is `nearly`, canonical `button`;
|
|
932
|
+
* `Card` `exists` as `card`. The first version of the frontier kebab-cased the
|
|
933
|
+
* name and looked for `ds-tag`, which does not exist - so a component whose
|
|
934
|
+
* recipe was sitting right there drew as a grey chip (dono, 01/08).
|
|
935
|
+
*/
|
|
936
|
+
const crosswalked = new Map();
|
|
937
|
+
for (const c of census.components ?? []) {
|
|
938
|
+
if (c.from)
|
|
939
|
+
continue; // a package's component is not one of theirs
|
|
940
|
+
const target = c.canonical ?? (c.bucket === "exclusive" ? c.name : null);
|
|
941
|
+
if (target)
|
|
942
|
+
crosswalked.set(c.name, safePartName(target));
|
|
943
|
+
}
|
|
944
|
+
const resolveRef = (name) => crosswalked.get(name) ?? null;
|
|
911
945
|
for (const [component, entry] of Object.entries(read)) {
|
|
912
|
-
const
|
|
913
|
-
|
|
946
|
+
const anatomy = entry?.anatomy;
|
|
947
|
+
const resolved = Array.isArray(anatomy) && anatomy.length > 0
|
|
948
|
+
? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root)
|
|
949
|
+
: Array.isArray(entry?.parts) && entry.parts.length > 0
|
|
950
|
+
? resolveFlatParts(entry.parts, declared)
|
|
951
|
+
: null;
|
|
952
|
+
if (!resolved)
|
|
914
953
|
continue;
|
|
915
|
-
const
|
|
916
|
-
if (
|
|
954
|
+
const hasParts = Object.keys(resolved.parts).length > 0;
|
|
955
|
+
if (!hasParts && resolved.tree.length === 0)
|
|
917
956
|
continue;
|
|
918
957
|
const look = looks[component];
|
|
958
|
+
const carried = {
|
|
959
|
+
...(hasParts ? { parts: resolved.parts } : {}),
|
|
960
|
+
...(resolved.tree.length > 0 ? { tree: resolved.tree } : {}),
|
|
961
|
+
...(resolved.composes.length > 0 ? { composes: resolved.composes } : {}),
|
|
962
|
+
...(resolved.external.length > 0 ? { external: resolved.external } : {}),
|
|
963
|
+
...(resolved.root ? { root: resolved.root } : {}),
|
|
964
|
+
};
|
|
919
965
|
looks[component] = look
|
|
920
|
-
? {
|
|
966
|
+
? {
|
|
967
|
+
...look,
|
|
968
|
+
...carried,
|
|
969
|
+
...(hasParts
|
|
970
|
+
? { parts: { ...(look.parts ?? {}), ...resolved.parts } }
|
|
971
|
+
: {}),
|
|
972
|
+
}
|
|
921
973
|
: {
|
|
922
974
|
base: {},
|
|
923
975
|
dark: {},
|
|
@@ -926,14 +978,37 @@ function resolveReadParts(census) {
|
|
|
926
978
|
literals: [],
|
|
927
979
|
fromToken: 0,
|
|
928
980
|
fromLiteral: 0,
|
|
929
|
-
|
|
981
|
+
...carried,
|
|
930
982
|
};
|
|
931
|
-
named += Object.keys(resolved).length;
|
|
983
|
+
named += Object.keys(resolved.parts).length;
|
|
984
|
+
if (resolved.tree.length > 0)
|
|
985
|
+
shaped += 1;
|
|
986
|
+
edges += resolved.composes.length;
|
|
987
|
+
for (const e of resolved.external)
|
|
988
|
+
libs.add(e.from);
|
|
989
|
+
for (const n of resolved.notes)
|
|
990
|
+
notes.add(n);
|
|
932
991
|
}
|
|
933
|
-
|
|
992
|
+
const total = Object.keys(read).length;
|
|
993
|
+
if (named > 0 || shaped > 0) {
|
|
934
994
|
console.log("");
|
|
935
|
-
console.log(body(`${named} part${named === 1 ? "" : "s"} your reading named across ${
|
|
995
|
+
console.log(body(`${named} part${named === 1 ? "" : "s"} your reading named across ${total} component${total === 1 ? "" : "s"} - each previews as what it IS rather than as a text box`));
|
|
996
|
+
}
|
|
997
|
+
if (shaped > 0) {
|
|
998
|
+
console.log(body(`${shaped} of them arrived with a SHAPE - the parts nested as they nest in your code, so the preview is the component and not a row of labels`));
|
|
999
|
+
}
|
|
1000
|
+
// A frontier is worth its own line, because it is the half nobody else has:
|
|
1001
|
+
// the edge is a rule an agent can obey, not a picture.
|
|
1002
|
+
if (edges > 0) {
|
|
1003
|
+
console.log(body(`${edges} place${edges === 1 ? "" : "s"} where one of your components is built out of another - ${edges === 1 ? "it becomes a rule" : "each becomes a rule"} about the two of them, not a guess`));
|
|
1004
|
+
}
|
|
1005
|
+
if (libs.size > 0) {
|
|
1006
|
+
const named_ = [...libs].slice(0, 3).join(", ");
|
|
1007
|
+
console.log(body(`${libs.size} librar${libs.size === 1 ? "y" : "ies"} your components need (${named_}) - ${libs.size === 1 ? "it does" : "they do"} not render here, and what to know about ${libs.size === 1 ? "it" : "them"} travels as rules`));
|
|
936
1008
|
}
|
|
1009
|
+
// NO SILENT FIX. A correction made quietly is a lie about what they wrote.
|
|
1010
|
+
for (const n of notes)
|
|
1011
|
+
console.log(body(n));
|
|
937
1012
|
}
|
|
938
1013
|
export async function runImport(opts) {
|
|
939
1014
|
const root = opts.root ?? process.cwd();
|
|
@@ -981,7 +1056,7 @@ export async function runImport(opts) {
|
|
|
981
1056
|
* Resolved here rather than at measure time because the reading arrives
|
|
982
1057
|
* AFTER the census was taken - this is the first moment both halves exist.
|
|
983
1058
|
*/
|
|
984
|
-
resolveReadParts(census);
|
|
1059
|
+
await resolveReadParts(census, root);
|
|
985
1060
|
}
|
|
986
1061
|
else {
|
|
987
1062
|
console.log(section("Reading your project"));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -65,6 +65,20 @@ const TOOLS = [
|
|
|
65
65
|
description: "The components this design system defines, with what each is for. Look here BEFORE writing any UI element from scratch - if something covers the purpose, materialize it with add_component instead.",
|
|
66
66
|
inputSchema: { type: "object", properties: {} },
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: "describe_component",
|
|
70
|
+
description: "Everything the system knows about ONE component: what it is made of, what it composes, WHICH LIBRARIES IT NEEDS, and the rules that govern it. Call this before writing code that uses a component - `list_components` gives you a name and a sentence, and a name does not tell you that the editor needs @tiptap/react or that a card is built out of a metric card.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
name: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Component name from list_components.",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ["name"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
68
82
|
{
|
|
69
83
|
name: "add_component",
|
|
70
84
|
description: "Materialize a component from the design system as real typed code in this project, ready to import and extend. Use it yourself - the person who asked for a feature should never have to know component names.",
|
|
@@ -191,6 +205,109 @@ async function listComponents(root) {
|
|
|
191
205
|
...rows.sort(),
|
|
192
206
|
].join("\n");
|
|
193
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* ONE COMPONENT, IN FULL - the round trip that did not exist.
|
|
210
|
+
*
|
|
211
|
+
* `list_components` returns a name and a description, so an agent building with
|
|
212
|
+
* `ds-text-editor` had no way to know it needs `@tiptap/react` before writing the
|
|
213
|
+
* first line. The anatomy, the composition chain and the required libraries are all
|
|
214
|
+
* in the installed contract; nothing was asking for them.
|
|
215
|
+
*
|
|
216
|
+
* It is also the confirmation the skill never had. After an import, reading this
|
|
217
|
+
* back says what the platform UNDERSTOOD - so a reading that came out thin is
|
|
218
|
+
* visible instead of being discovered later in a preview.
|
|
219
|
+
*/
|
|
220
|
+
async function describeComponent(root, name) {
|
|
221
|
+
const { documents, requires } = await loadSystem(root);
|
|
222
|
+
let recipe;
|
|
223
|
+
for (const doc of documents) {
|
|
224
|
+
const d = doc;
|
|
225
|
+
recipe = d.components?.[name] ?? d.blocks?.[name];
|
|
226
|
+
if (recipe)
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (!recipe) {
|
|
230
|
+
return `This system has no component called "${name}". Run list_components to see what it does have - and if nothing covers what you need, file request_component rather than writing one from scratch.`;
|
|
231
|
+
}
|
|
232
|
+
const out = [
|
|
233
|
+
`${name}${recipe.description ? ` - ${recipe.description}` : ""}`,
|
|
234
|
+
];
|
|
235
|
+
// WHAT IT SITS ON. 17 of 23 components in a real library carry no surface of
|
|
236
|
+
// their own, because the surface belongs to what they return.
|
|
237
|
+
const p = recipe.preview;
|
|
238
|
+
if (p?.rootRef) {
|
|
239
|
+
out.push("", `Sits on: ${p.rootRef}${p.rootName && p.rootName !== p.rootRef ? ` (your ${p.rootName})` : ""} - its background, border and radius come from there, not from this component.`);
|
|
240
|
+
}
|
|
241
|
+
else if (p?.rootFrom) {
|
|
242
|
+
out.push("", `Sits on: ${p.rootFrom} - a third-party root. Its markup and behaviour are that library's.`);
|
|
243
|
+
}
|
|
244
|
+
// WHAT IT IS MADE OF, and where it ends.
|
|
245
|
+
const composes = [];
|
|
246
|
+
const libs = [];
|
|
247
|
+
const shape = [];
|
|
248
|
+
const walk = (nodes, depth) => {
|
|
249
|
+
for (const raw of nodes) {
|
|
250
|
+
const n = raw;
|
|
251
|
+
const pad = " ".repeat(depth + 1);
|
|
252
|
+
if (n.as === "component" && n.ref) {
|
|
253
|
+
shape.push(`${pad}<${n.ref}>${n.refName && n.refName !== n.ref ? ` (your ${n.refName})` : ""}`);
|
|
254
|
+
if (!composes.includes(n.ref))
|
|
255
|
+
composes.push(n.ref);
|
|
256
|
+
}
|
|
257
|
+
else if (n.as === "external" && n.from) {
|
|
258
|
+
shape.push(`${pad}${n.from} (third party - not ours to render)`);
|
|
259
|
+
if (!libs.includes(n.from))
|
|
260
|
+
libs.push(n.from);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
shape.push(`${pad}${n.as}${n.part ? ` .${name}-${n.part}` : ""}`);
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(n.children))
|
|
266
|
+
walk(n.children, depth + 1);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
if (Array.isArray(p?.parts) && p.parts.length > 0) {
|
|
270
|
+
walk(p.parts, 0);
|
|
271
|
+
out.push("", "Made of:", ...shape);
|
|
272
|
+
}
|
|
273
|
+
else if (recipe.parts && Object.keys(recipe.parts).length > 0) {
|
|
274
|
+
out.push("", `Parts: ${Object.keys(recipe.parts).join(", ")} - compose them inside it.`);
|
|
275
|
+
}
|
|
276
|
+
if (composes.length > 0) {
|
|
277
|
+
out.push("", `Built out of: ${composes.join(", ")}. Change one of those in one place rather than reproducing it here, and call describe_component on it before you do.`);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* WHAT IT NEEDS INSTALLED - the whole reason this tool earns its place.
|
|
281
|
+
*
|
|
282
|
+
* From `requires.json`, filtered by the stack at install time, so the answer is
|
|
283
|
+
* about THIS project. Never installs: a dependency has a licence, a bundle cost
|
|
284
|
+
* and a maintainer attached, so the agent reports and the person decides.
|
|
285
|
+
*/
|
|
286
|
+
const needed = requires.filter((r) => (r.applies ?? []).includes(name));
|
|
287
|
+
if (needed.length > 0 || libs.length > 0) {
|
|
288
|
+
const named = [
|
|
289
|
+
...new Set([...needed.map((r) => r.requires), ...libs]),
|
|
290
|
+
];
|
|
291
|
+
out.push("", `Needs installed: ${named.join(", ")}.`, "Check the manifest before you write against it. If it is missing, say so and ASK - do not install it yourself.");
|
|
292
|
+
for (const r of needed) {
|
|
293
|
+
if (r.pinned)
|
|
294
|
+
out.push(` ${r.requires} ${r.pinned}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// THE RULES, scoped. A relation names both, which is what an agent needs in
|
|
298
|
+
// order not to assemble it wrongly.
|
|
299
|
+
const laws = [...(recipe.usage ?? [])];
|
|
300
|
+
if (laws.length > 0) {
|
|
301
|
+
out.push("", "Rules for this component:", ...laws.map((l) => ` ${l}`));
|
|
302
|
+
}
|
|
303
|
+
if (needed.length > 0) {
|
|
304
|
+
out.push(...needed.map((r) => ` ${r.text}`));
|
|
305
|
+
}
|
|
306
|
+
if (laws.length === 0 && needed.length === 0) {
|
|
307
|
+
out.push("", "No rules govern this component yet. Build with it, and if you find yourself working around it, file request_component with the reasoning.");
|
|
308
|
+
}
|
|
309
|
+
return out.join("\n");
|
|
310
|
+
}
|
|
194
311
|
async function addComponent(root, name) {
|
|
195
312
|
const { table } = await loadSystem(root);
|
|
196
313
|
if (!table.slug)
|
|
@@ -207,6 +324,8 @@ async function callTool(root, name, args) {
|
|
|
207
324
|
return text(await findToken(root, String(args.value ?? "")));
|
|
208
325
|
case "list_components":
|
|
209
326
|
return text(await listComponents(root));
|
|
327
|
+
case "describe_component":
|
|
328
|
+
return text(await describeComponent(root, String(args.name ?? "")));
|
|
210
329
|
case "add_component":
|
|
211
330
|
return text(await addComponent(root, String(args.name ?? "")));
|
|
212
331
|
case "request_component": {
|
package/dist/commands/refit.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, join } from "node:path";
|
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { body, section, snippet } from "../output.js";
|
|
6
|
+
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
6
7
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
7
8
|
/** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
|
|
8
9
|
* a folder holding only refit artifacts doesn't count). */
|
|
@@ -132,7 +133,7 @@ export async function refit(file, opts) {
|
|
|
132
133
|
if (config.target === "next") {
|
|
133
134
|
const compDir = join(root, config.componentsDir, res.name);
|
|
134
135
|
await mkdir(compDir, { recursive: true });
|
|
135
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles);
|
|
136
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug));
|
|
136
137
|
for (const f of files) {
|
|
137
138
|
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
138
139
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -4,6 +4,7 @@ import { generateComponentFiles } from "../component-codegen.js";
|
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
|
|
6
6
|
import { body, section, snippet } from "../output.js";
|
|
7
|
+
import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
|
|
7
8
|
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
8
9
|
import { add } from "./add.js";
|
|
9
10
|
/**
|
|
@@ -151,7 +152,11 @@ export async function upgrade(slug, opts) {
|
|
|
151
152
|
continue;
|
|
152
153
|
try {
|
|
153
154
|
const res = await fetchComponent(base, slug, entry);
|
|
154
|
-
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles
|
|
155
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles,
|
|
156
|
+
// Both were missing here, and `upgrade` is the command that REWRITES
|
|
157
|
+
// components somebody already has: without the convention it would have
|
|
158
|
+
// taken a working component and stripped its styles.
|
|
159
|
+
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)));
|
|
155
160
|
for (const file of files) {
|
|
156
161
|
await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
|
|
157
162
|
}
|