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 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({ slug: lock.slug, name: lock.name, version: lock.version });
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
- const head = `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) - guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``;
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
- const body = `## Design Systems (via SynthesisUI)
95
-
96
- This project uses design system(s) brought in by the \`synthesisui\` CLI. **When creating or editing
97
- components, read the system's GUIDE.md and follow it:** use only semantic tokens
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>\` to update._`;
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
  /**
@@ -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
+ }
@@ -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 { table: buildTable({ css, lock }), recipes, documents };
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 === "yours"
304
- ? `Your own tokens - ${plural(table.byName.size, "token")} found, ${plural(d.scanned, "file")} read`
305
- : `No system installed - ${plural(d.scanned, "file")} read`));
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) {
@@ -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 === "yours" ? parseRootTokens(input.css) : parseTokens(input.css);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
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": {