synthesisui 0.16.292 → 0.16.294

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.
@@ -1,15 +1,17 @@
1
1
  import { access, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { syncClaudeMd } from "../claude-md.js";
4
- import { readProjectConfig, resolveRegistry } from "../config.js";
4
+ import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
5
5
  import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
6
6
  import { lockReference } from "../group-role.js";
7
7
  import { buildGuide } from "../guide.js";
8
8
  import { censusScope } from "../measured-scope.js";
9
9
  import { body as line, section, snippet } from "../output.js";
10
10
  import { fetchDesignSystem } from "../registry.js";
11
+ import { repoStateOf } from "../repo-state.js";
11
12
  import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.js";
12
13
  import { detectStack } from "../stack.js";
14
+ import { onlyWhatMatched } from "../their-theme.js";
13
15
  import { pointTokensAtTheirNames } from "../their-vars.js";
14
16
  /**
15
17
  * QUAL METADE DESTA PASTA UM TIME COMMITA.
@@ -203,9 +205,26 @@ export async function add(slug, opts) {
203
205
  * do servidor. A folha nunca fica pior do que estava.
204
206
  */
205
207
  const theirVars = await pointTokensAtTheirNames(projectRoot, payload);
208
+ /**
209
+ * O `theme.css` ALINHA ONDE HÁ O QUE ALINHAR - ver `onlyWhatMatched`.
210
+ *
211
+ * O QUE ISSO EVITA NA TELA DELE: `rounded-xl` valia 12px no código dele, o default do Tailwind, e
212
+ * a nossa folha o fazia valer 28px em 21 elementos. Ele não pediu e não declarou raio nenhum: a
213
+ * classe dele significava uma coisa e passava a significar outra. Medido em 23/08 - dos 68
214
+ * utilitários que o arquivo redefine, 4 apontavam para um token que casou com o vocabulário dele e
215
+ * 64 eram nossos.
216
+ *
217
+ * Só isto é possível porque o mapa existe. Sem ele, "este token é dele?" não tinha resposta na hora
218
+ * de escrever a folha.
219
+ */
220
+ const aligned = onlyWhatMatched(payload.artifacts["theme.css"] ?? "", new Set(theirVars.pairs.map((p) => p.ours)));
206
221
  // 1. server artifacts (tokens.css, theme.css, …) → pinned version folder
207
222
  for (const [filename, content] of Object.entries(payload.artifacts)) {
208
- await writeFile(join(versionDir, filename), filename === "tokens.css" ? theirVars.css : content, "utf8");
223
+ await writeFile(join(versionDir, filename), filename === "tokens.css"
224
+ ? theirVars.css
225
+ : filename === "theme.css"
226
+ ? aligned.css
227
+ : content, "utf8");
209
228
  }
210
229
  // 2. canonical source of truth
211
230
  await writeFile(join(versionDir, "design-system.json"), `${JSON.stringify(payload.document, null, 2)}\n`, "utf8");
@@ -301,6 +320,33 @@ export async function add(slug, opts) {
301
320
  fetchedAt: landed || !prev?.fetchedAt ? new Date().toISOString() : prev.fetchedAt,
302
321
  };
303
322
  await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
323
+ /**
324
+ * O MAPA SOBE AGORA, e é o único momento em que ele é novo.
325
+ *
326
+ * O QUE ESTAVA FALTANDO, medido em 23/08: o `.lock` tinha 24 pares e a tabela `token_names` tinha
327
+ * zero. O `repo_state` viaja em três caminhos - o import, o ping do agente e o `sync` - e o import
328
+ * o envia ANTES de instalar, quando o `.lock` ainda não existe. Então o mapa ficava esperando o
329
+ * próximo comando que reportasse, e até lá o Studio continuava falando a nossa língua.
330
+ *
331
+ * Este é o comando que escreve o mapa. Ele é quem tem que contar.
332
+ *
333
+ * GUARDADO: o sistema está instalado e os arquivos estão no lugar. Uma rede que cai aqui custa o
334
+ * mapa no servidor até o próximo `sync`, e jamais o install que acabou de dar certo.
335
+ */
336
+ if (opts.cli) {
337
+ const state = await repoStateOf(projectRoot, payload.slug, opts.cli).catch(() => null);
338
+ const token = await readToken();
339
+ if (state?.tokenMap && token) {
340
+ await fetch(`${base}/api/ledger`, {
341
+ method: "POST",
342
+ headers: {
343
+ "content-type": "application/json",
344
+ Authorization: `Bearer ${token}`,
345
+ },
346
+ body: JSON.stringify({ slug: payload.slug, repo: state, events: [] }),
347
+ }).catch(() => null);
348
+ }
349
+ }
304
350
  await writeGovernanceIgnore(projectRoot);
305
351
  const retired = await retireMaterializedDoctrine(slugDir);
306
352
  // 5b. governance rules (personal DS) → doctrine.json at the slug root (stable path,
@@ -419,6 +465,15 @@ export async function add(slug, opts) {
419
465
  .map((r) => `${r.theirs} → ${theirVars.pairs.find((p) => p.ours === r.ours)?.theirs ?? "?"}`)
420
466
  .join(", ")}${renamed.length > 3 ? ", …" : ""}. The system follows the new name from here.`);
421
467
  }
468
+ /**
469
+ * E ELE FICA SABENDO DO QUE NÃO FOI ALINHADO - cortar em silêncio é a outra metade do erro.
470
+ *
471
+ * A linha diz quantos utilitários do Tailwind ficaram apontando para a decisão dele e quantos
472
+ * saíram por serem nossos, com os primeiros nomes. Quem lê pode discordar de qualquer um.
473
+ */
474
+ if (aligned.dropped.length > 0) {
475
+ console.log(line(` ${aligned.kept} Tailwind utilit${aligned.kept === 1 ? "y" : "ies"} now point at your own decisions; ${aligned.dropped.length} were left alone because the value would be ours (${aligned.dropped.slice(0, 3).join(", ")}${aligned.dropped.length > 3 ? ", …" : ""}) - your classes keep meaning what they mean today.`));
476
+ }
422
477
  if (theirVars.pointed > 0)
423
478
  console.log(` ${theirVars.pointed} value${theirVars.pointed === 1 ? "" : "s"} in tokens.css now point at the name YOUR code already gives ${theirVars.pointed === 1 ? "it" : "them"} - change yours and the system follows${theirVars.pruned > 0 ? `; ${theirVars.pruned} matched but your build does not emit ${theirVars.pruned === 1 ? "that name" : "those names"}, so ${theirVars.pruned === 1 ? "it keeps" : "they keep"} the value` : ""}`);
424
479
  /**
@@ -436,7 +491,21 @@ export async function add(slug, opts) {
436
491
  /** Nomeado, nunca em silêncio: apagar arquivo no repo de alguém se diz em voz alta. */
437
492
  if (retired.length > 0)
438
493
  console.log(` removed ${retired.join(" and ")} - nothing rewrote them after an install, so they stated an older version's rules as current`);
439
- console.log(` CLAUDE.md ${claudeMd.created ? "created" : "updated"} (${claudeMd.count} system(s) installed)`);
494
+ /**
495
+ * "CREATED" PRECISA DIZER A CONSEQUÊNCIA, e a palavra sozinha não dizia.
496
+ *
497
+ * A distinção já existia aqui - `created` contra `updated` -, e três vezes em 23/08 o arquivo foi
498
+ * criado porque ele não estava no disco, com 78 linhas dele vivas no git. A palavra `created` é
499
+ * neutra: ela descreve o que o comando fez e não o que aconteceu com o trabalho de quem lê.
500
+ *
501
+ * `updated` significa "o seu arquivo continua aí, com o nosso bloco dentro". `created` significa
502
+ * "não havia arquivo aqui" - e num repositório que TEM um versionado, isso quer dizer que ele está
503
+ * ausente do diretório de trabalho, o que ninguém faz de propósito. A linha passa a dizer isso e o
504
+ * comando de volta.
505
+ */
506
+ console.log(claudeMd.created
507
+ ? ` CLAUDE.md created - there was none here (${claudeMd.count} system(s) indexed). If your repo has one in git, it is missing from your working tree: \`git checkout -- CLAUDE.md\` and run this again to keep both.`
508
+ : ` CLAUDE.md updated - your file is intact, with our block inside it (${claudeMd.count} system(s) indexed)`);
440
509
  if (opts.setupHints === false)
441
510
  return;
442
511
  const hasTheme = cssArtifacts.includes("theme.css");
@@ -34,6 +34,22 @@ async function writeCn(root, compDir, slug) {
34
34
  /** Slugs/names are kebab-case by contract; reject anything else before it ever
35
35
  * reaches a filesystem path (defense-in-depth against `../` traversal). */
36
36
  const SAFE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
37
+ /**
38
+ * O NOME QUE O ARQUIVO VAI TER - o do blueprint, ou o que ele pediu.
39
+ *
40
+ * Puro e exportado porque é a decisão, e ela precisa ser provada sem rede nem disco. O nome vem da
41
+ * linha de comando e termina dentro de um `join`, então a validação é defesa em profundidade e não
42
+ * capricho: `--as ../escape` não pode chegar ao sistema de arquivos.
43
+ */
44
+ export function localName(blueprint, asked) {
45
+ const wanted = asked?.trim();
46
+ if (!wanted)
47
+ return blueprint;
48
+ if (!SAFE_NAME.test(wanted)) {
49
+ throw new RegistryError(`Invalid --as "${wanted}" - use kebab-case, like \`--as my-${blueprint}\`.`);
50
+ }
51
+ return wanted;
52
+ }
37
53
  /**
38
54
  * Brings ONE component from a design system into the project (granular "bring
39
55
  * specific", INS-18 fatia 3):
@@ -172,7 +188,15 @@ export async function component(slug, name, opts) {
172
188
  console.log(body("Nothing of yours was touched. Which name it takes is your call, not ours."));
173
189
  return;
174
190
  }
175
- const compDir = join(root, config.componentsDir, res.name);
191
+ /**
192
+ * O NOME QUE O ARQUIVO VAI TER - ver `localName`.
193
+ *
194
+ * `--as` é o que permite ele VER o que um blueprint produz num repositório que já tem o
195
+ * componente. O CSS não muda de nome: a receita é a do blueprint, e as classes dela são
196
+ * `.ds-<blueprint>` - o componente novo veste o mesmo desenho, com o nome dele no arquivo.
197
+ */
198
+ const local = localName(res.name, opts.as);
199
+ const compDir = join(root, config.componentsDir, local);
176
200
  await mkdir(compDir, { recursive: true });
177
201
  let filenames;
178
202
  if (wantInteractive) {
@@ -180,10 +204,10 @@ export async function component(slug, name, opts) {
180
204
  // it wears (.css) + the barrel. Ignores the css|tailwind flavor - the
181
205
  // template drives itself off the .ds-* classes.
182
206
  const tsx = interactiveTemplate(res.name);
183
- await writeFile(join(compDir, `${res.name}.tsx`), tsx, "utf8");
184
- await writeFile(join(compDir, `${res.name}.css`), `${css}\n`, "utf8");
185
- await writeFile(join(compDir, "index.ts"), `export * from "./${res.name}";\n`, "utf8");
186
- filenames = [`${res.name}.tsx`, `${res.name}.css`, "index.ts"];
207
+ await writeFile(join(compDir, `${local}.tsx`), tsx, "utf8");
208
+ await writeFile(join(compDir, `${local}.css`), `${css}\n`, "utf8");
209
+ await writeFile(join(compDir, "index.ts"), `export * from "./${local}";\n`, "utf8");
210
+ filenames = [`${local}.tsx`, `${local}.css`, "index.ts"];
187
211
  }
188
212
  else {
189
213
  const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, config.styles, await reactMajorOf(root),
@@ -195,7 +219,17 @@ export async function component(slug, name, opts) {
195
219
  * it, so the TSX and the stylesheet in the same folder agree by
196
220
  * construction. Falling back to disk keeps an older registry working.
197
221
  */
198
- res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
222
+ res.classNames ?? (await readInstalledConvention(root, slug)),
223
+ /**
224
+ * O NOME LOCAL, e o codegen já sabia fazer isto - ver `localName` em
225
+ * `component-codegen.ts`: *"A project that already exports `Button` should not have to give
226
+ * the name up to install ours"*. A capacidade existia e nenhuma flag a alcançava, então a
227
+ * recusa oferecia `--as` e o comando ignorava.
228
+ *
229
+ * O ARQUIVO e o EXPORT levam o nome dele; a CLASSE continua sendo a do blueprint. Um
230
+ * `<CardPreview>` vestindo `.ds-card` está estilizado certo e não faz sombra em nada dele.
231
+ */
232
+ local, await readInstalledScheme(root, slug));
199
233
  /**
200
234
  * AS DECLARAÇÕES DO ARQUIVO DELE QUE O INTERPRETADOR NÃO LEU - ver `unread-for-component.ts`.
201
235
  *
@@ -217,7 +251,7 @@ export async function component(slug, name, opts) {
217
251
  filenames = files.map((f) => f.filename);
218
252
  }
219
253
  const flavor = wantInteractive ? "interactive" : `styles: ${config.styles}`;
220
- console.log(`✓ ${config.componentsDir}/${res.name}/ → ${filenames.join(", ")} (${flavor})`);
254
+ console.log(`✓ ${config.componentsDir}/${local}/ → ${filenames.join(", ")} (${flavor})${local === res.name ? "" : ` - the "${res.name}" blueprint, under your name`}`);
221
255
  }
222
256
  else if (opts.interactive && !hasInteractiveTemplate(res.name)) {
223
257
  console.log(` note: no interactive template for "${res.name}" - materialized the standard shell.`);
package/dist/index.js CHANGED
@@ -522,6 +522,14 @@ async function main() {
522
522
  version,
523
523
  artifactsOnly: flags["artifacts-only"] === true,
524
524
  interactive: flags.interactive === true,
525
+ /**
526
+ * O NOME LOCAL - ver `localName` em `component.ts`.
527
+ *
528
+ * A recusa por nome ocupado já oferecia `--as`, e a flag não era lida: quem seguia a
529
+ * instrução da tela recebia o arquivo com o nome de sempre.
530
+ */
531
+ ...(typeof flags.as === "string" ? { as: flags.as } : {}),
532
+ ...(flags.force === true ? { force: true } : {}),
525
533
  });
526
534
  break;
527
535
  }
@@ -128,7 +128,7 @@
128
128
  * é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
129
129
  * publicar no meio.
130
130
  */
131
- export const MATERIALISER_SINCE = "0.16.292";
131
+ export const MATERIALISER_SINCE = "0.16.293";
132
132
  /**
133
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
134
  *
@@ -0,0 +1,17 @@
1
+ /** `--radius-lg: var(--ds-radius-lg);` - uma redefinição de utilitário do Tailwind pelo nosso token. */
2
+ const ALIGNMENT = /^([ \t]*)(--[a-zA-Z0-9-]+)(\s*:\s*)var\(\s*(--ds-[a-zA-Z0-9-]+)\s*\)\s*;[ \t]*\n?/gm;
3
+ export function onlyWhatMatched(css,
4
+ /** Os nossos tokens que casaram com um nome do código dele - ver `PointedAt.pairs`. */
5
+ matched) {
6
+ let kept = 0;
7
+ const dropped = [];
8
+ const out = css.replace(ALIGNMENT, (whole, _indent, tailwind, _sep, ours) => {
9
+ if (matched.has(ours)) {
10
+ kept += 1;
11
+ return whole;
12
+ }
13
+ dropped.push(tailwind);
14
+ return "";
15
+ });
16
+ return { css: out, kept, dropped: dropped.sort() };
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.292",
3
+ "version": "0.16.294",
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": {