synthesisui 0.16.411 → 0.16.412

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.
@@ -8,10 +8,11 @@ import { detectAppDirs } from "../global-sheet.js";
8
8
  import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
9
9
  import { body, section, snippet } from "../output.js";
10
10
  import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
11
- import { fetchComponent, RegistryError } from "../registry.js";
11
+ import { fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
12
  import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
13
13
  import { flavourResolver } from "../styles-flavour.js";
14
- import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
14
+ import { inTheirTongue, projectTongue, sumSpoken, tongueFromArtifacts, } from "../their-tongue.js";
15
+ import { resolvableVars } from "../their-vars.js";
15
16
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
16
17
  import { readWiring } from "../wiring-read.js";
17
18
  import { editedSinceWritten, readWritten, recordWritten } from "../written.js";
@@ -114,7 +115,38 @@ export async function component(slug, name, opts) {
114
115
  * apagaria a cor. Um projeto de destino chega sem mapa no `.lock`, nada é traduzido, e a folha
115
116
  * continua sendo o caminho - o comando DIZ qual dos dois aconteceu.
116
117
  */
117
- const tongue = await projectTongue(root, slug);
118
+ const installedTongue = await projectTongue(root, slug);
119
+ /**
120
+ * E SEM O `add`, O SISTEMA VEM DO REGISTRY E O VOCABULARIO E' DERIVADO EM MEMORIA.
121
+ *
122
+ * O QUE ACONTECIA (medido em 10/09): sem `.lock`, `projectTongue` responde `null`, nada era
123
+ * traduzido, e o arquivo saia com `var(--ds-*)` cru - inutil ate' que a pessoa rodasse `add` e
124
+ * materializasse 1813 linhas, 111 variaveis e 628 referencias no repositorio dela. Pedir UM
125
+ * botao passava por instalar o sistema inteiro.
126
+ *
127
+ * A NOSSA FOLHA E' A REFERENCIA, NAO O PRE-REQUISITO (a definicao do produto, 07/09): ela existe
128
+ * para o agente dele ler e construir sobre ela. A medicao que ela carregava - quais das nossas
129
+ * variaveis tem nome no codigo dele - nao precisa de disco: e' a mesma varredura que o `add`
130
+ * faz, feita aqui e jogada fora depois de traduzir.
131
+ *
132
+ * UMA IDA A MAIS A' REDE, e so' neste caminho: quem ja' instalou o sistema le' do `.lock` como
133
+ * sempre. O `catch` mantem o comportamento antigo - sem tradução, e o bloco de setup abaixo diz
134
+ * que a folha e' o caminho.
135
+ */
136
+ const fromRegistry = installedTongue
137
+ ? null
138
+ : await fetchDesignSystem(base, slug, opts.version).catch(() => null);
139
+ /** A leitura do sistema NAO chegou - rede fora, ou sistema privado sem sessao. Ver o relato. */
140
+ const installedSheetMissing = !installedTongue && fromRegistry === null;
141
+ /**
142
+ * A NOSSA FOLHA ESTA' NA PASTA DELE? - o `.lock` responde, e e' o mesmo arquivo que o `add`
143
+ * escreve. Desde o A1 esta pergunta deixou de ser retorica: `component` roda sem ele.
144
+ */
145
+ const installedHere = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8").then(() => true, () => false);
146
+ const tongue = installedTongue ??
147
+ (fromRegistry
148
+ ? await tongueFromArtifacts(root, fromRegistry.artifacts)
149
+ : null);
118
150
  /**
119
151
  * O RELATÓRIO SÓ EXISTE DEPOIS DE OS BYTES EXISTIREM - e é essa ordem que corrige o defeito.
120
152
  *
@@ -422,6 +454,45 @@ export async function component(slug, name, opts) {
422
454
  console.log(spoken.left.length === 0
423
455
  ? ` No variable in what was just written points at our stylesheet - they are all names YOUR code declares.`
424
456
  : ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css carries ${spoken.left.length === 1 ? "it" : "those"}.`);
457
+ /**
458
+ * E DIZER DE ONDE VEIO O VOCABULARIO, quando o sistema NAO esta' instalado aqui.
459
+ *
460
+ * Sem esta linha o resultado seria magico: o arquivo fala a lingua dele e nada explica como.
461
+ * A frase importa porque a conclusao natural - *"entao o `add` ja' rodou"* - esta' errada, e a
462
+ * pessoa precisa saber que o que ela tem e' o componente, e que a folha continua sendo uma
463
+ * escolha e nao uma pendencia.
464
+ */
465
+ if (fromRegistry)
466
+ console.log(` Nothing of ours was installed to do that - the system was read at v${fromRegistry.version}, and the names came from your own code.`);
467
+ }
468
+ else if (!tongue) {
469
+ /**
470
+ * A LACUNA DECLARADA (lei 8): nada foi traduzido, e ISSO SE DIZ.
471
+ *
472
+ * O QUE ACONTECIA SEM ESTA LINHA: o `.tsx` saia com `var(--ds-*)` cru e o terminal ficava
473
+ * mudo. A pessoa abre o arquivo, ve' variaveis que o `globals.css` dela nao declara, e a
474
+ * unica forma de descobrir por que a cor nao apareceu e' ir ler o nosso codigo.
475
+ *
476
+ * E OS DOIS MOTIVOS SAO DIFERENTES, entao a frase distingue: o repositorio dela nao nomeia
477
+ * nenhum destes valores (o caso comum, e nao ha' o que fazer), ou a leitura do sistema nao
478
+ * chegou - rede fora, sistema privado sem sessao. O segundo tem conserto e o primeiro nao.
479
+ */
480
+ /**
481
+ * E AS TRES CAUSAS SAO DIFERENTES, entao a frase distingue - so' uma delas tem conserto na
482
+ * mao dele, e ela e' a mais provavel das tres.
483
+ *
484
+ * a leitura nao chegou rede fora, sessao ausente, sistema privado
485
+ * o build dele nao existe `resolvableVars` recusa todo par que o build nao emite, por
486
+ * desenho: um nome afirmado sem prova pinta a cor errada no dia
487
+ * em que ele o renomear. Num clone fresco isso e' o caso normal
488
+ * o codigo dele nao nomeia nada a unica que o silencio de fato descrevia
489
+ */
490
+ const noBuild = !installedSheetMissing && (await resolvableVars(root)) === null;
491
+ console.log(installedSheetMissing
492
+ ? ` Nothing was translated: "${slug}" could not be read from the registry just now, so the variables stayed as ours. Check your connection, or run: npx synthesisui login`
493
+ : noBuild
494
+ ? ` Nothing was translated: this project has no build output to read, and a name we cannot see your build emit is a name we will not write. Run your build once and ask for it again - the file then speaks your own names.`
495
+ : ` Nothing was translated: your code names none of the values this system declares yet, so the variables stayed as ours and tokens.css is what resolves them.`);
425
496
  }
426
497
  // ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
427
498
  const tailwind = flavour === "tailwind";
@@ -454,7 +525,15 @@ export async function component(slug, name, opts) {
454
525
  */
455
526
  const need = whatOnlyTheSheetResolves({
456
527
  source: writtenSource,
457
- themeCss: await installedThemeCss(root, slug),
528
+ /**
529
+ * O `@theme` DO SISTEMA - do disco quando ele instalou, do registry quando nao.
530
+ *
531
+ * A pergunta que este bloco faz e' *"sobrou classe que so' o nosso @theme gera?"*. Sem o
532
+ * `add`, `installedThemeCss` responde vazio, e vazio leria como "nao sobrou nada" - uma
533
+ * resposta certa por acidente que ficaria errada no dia em que sobrasse.
534
+ */
535
+ themeCss: fromRegistry?.artifacts?.["theme.css"] ??
536
+ (await installedThemeCss(root, slug)),
458
537
  /** O vocabulário DELE sai da conta - ver `theirNames`. O mapa do `.lock` é a via mais barata. */
459
538
  theirNames: tongue?.names ? [...tongue.names.values()] : [],
460
539
  });
@@ -517,6 +596,19 @@ export async function component(slug, name, opts) {
517
596
  }
518
597
  if (need.needed && !alreadyWired) {
519
598
  console.log(section(`One-time setup (once per app, for "${slug}")`));
599
+ /**
600
+ * O PASSO ZERO VEM PRIMEIRO QUANDO A FOLHA NAO EXISTE - e desde o A1 este e' o caso comum.
601
+ *
602
+ * O QUE ACONTECIA: o passo 1 mandava colar `@import ".../tokens.css"` e o aviso de que o
603
+ * sistema nao esta' instalado saia na ULTIMA linha, entre parenteses. Quem nunca rodou `add`
604
+ * - agora um caminho normal, porque o `component` deixou de exigi-lo - lia a instrucao de
605
+ * cima para baixo e importava um caminho que nao existe. O erro do build nao fala de
606
+ * install: fala de um arquivo ausente.
607
+ *
608
+ * O motivo antes da instrucao, e nao depois dela.
609
+ */
610
+ if (!installedHere)
611
+ console.log(body(`First: "${slug}" is not installed in this project yet, so the file the import below points at does not exist. Run \`npx synthesisui add ${slug}\` before pasting it.`));
520
612
  console.log(body(need.variables.length > 0
521
613
  ? `(what still needs the sheet here: ${need.variables.slice(0, 3).join(", ")}${need.variables.length > 3 ? `, +${need.variables.length - 3}` : ""} - your code names no value for ${need.variables.length === 1 ? "it" : "them"})`
522
614
  : `(what still needs the sheet here: the ${need.classes.slice(0, 3).join(", ")}${need.classes.length > 3 ? `, +${need.classes.length - 3}` : ""} ${need.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates)`));
@@ -529,8 +621,10 @@ export async function component(slug, name, opts) {
529
621
  console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
530
622
  console.log("");
531
623
  console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
532
- console.log("");
533
- console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
624
+ if (installedHere) {
625
+ console.log("");
626
+ console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
627
+ }
534
628
  }
535
629
  /**
536
630
  * O ELEMENTO QUE SAIU NÃO ATIVA O QUE A RECEITA PEDE - e isso se diz, em vez de sumir (lei 8).
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { codexPinBefore, wireAgent } from "../agent-wiring.js";
4
4
  import { blockHomes, syncClaudeMd } from "../claude-md.js";
5
5
  import { resolveRegistry } from "../config.js";
6
+ import { notAProject, projectRootFrom } from "../is-a-project.js";
6
7
  import { fmt, say } from "../lang.js";
7
8
  import { body, bodyWrapped, paint, section, snippet } from "../output.js";
8
9
  import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
@@ -212,6 +213,25 @@ version) {
212
213
  }
213
214
  export async function connect(opts) {
214
215
  const root = opts.dir ?? process.cwd();
216
+ /**
217
+ * ANTES DA PRIMEIRA ESCRITA, SEMPRE - ver `is-a-project.ts`.
218
+ *
219
+ * MEDIDO EM 10/09: este comando tinha 6 escritas e 0 checagens de projeto. Rodado por engano
220
+ * na home ou numa pasta recem-criada, ele deixava `CLAUDE.md`, `.claude/settings.json`,
221
+ * `.mcp.json` e a casa da governanca ali - e um `CLAUDE.md` orfao instrui todo agente que
222
+ * abrir naquela pasta, semanas depois, sem ninguem lembrar de onde ele veio.
223
+ *
224
+ * AQUI E NAO DENTRO DE CADA ESCRITA: recusar na porta e' a unica forma de a promessa
225
+ * *"nada foi escrito"* ser verdade. Meia recusa deixaria dois arquivos e nenhuma mensagem.
226
+ */
227
+ const project = await projectRootFrom(root);
228
+ if (!project) {
229
+ console.log(section("Not a project"));
230
+ for (const line of notAProject("connect", root))
231
+ console.log(body(line));
232
+ process.exitCode = 1;
233
+ return;
234
+ }
215
235
  // Both unless one is explicitly turned off - somebody who says `--no-hook`
216
236
  // means it, and somebody who says nothing wants the thing to work.
217
237
  const want = { hook: opts.hook !== false, mcp: opts.mcp !== false };
@@ -7,7 +7,7 @@ import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalle
7
7
  import { postGenerate, RegistryError } from "../registry.js";
8
8
  import { flavourResolver } from "../styles-flavour.js";
9
9
  import { projectTongue } from "../their-tongue.js";
10
- import { recordWritten } from "../written.js";
10
+ import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
11
11
  /** PascalCase para o hint de import (course-card → CourseCard). */
12
12
  function pascalName(name) {
13
13
  return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
@@ -76,6 +76,23 @@ export async function generate(description, opts) {
76
76
  await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
77
77
  /** O VOCABULÁRIO DELE - um componente gerado cai no mesmo projeto e fala a mesma língua. */
78
78
  await projectTongue(root, slug), await installedThemeVars(root, slug));
79
+ /**
80
+ * A EDICAO DELE VENCE A REESCRITA (T7) - a leitura que faltava.
81
+ *
82
+ * MEDIDO EM 10/09: este comando GRAVAVA o fingerprint e nunca o lia. A promessa existia pela
83
+ * metade - o `upgrade` respeitava o arquivo dele, e o comando que o escreveu na primeira vez
84
+ * passava por cima na segunda. Um pedido parecido devolve o mesmo nome, e o ajuste dele some.
85
+ *
86
+ * O QUE ELE NAO PERDE AO SER RECUSADO: a receita e o CSS ja' estao no disco - so' o `.tsx`
87
+ * nao foi escrito. E a saida e' a mesma que o `component` oferece.
88
+ */
89
+ const edited = await theirEdits(join(root, "_synthesisui", "ds", slug), res.name, compDir);
90
+ if (edited && edited.length > 0 && !opts.force) {
91
+ console.log("");
92
+ console.log(keptLine(res.name, edited, `npx synthesisui generate "${description}" --name ${res.name} --force`));
93
+ console.log(` The recipe and the compiled CSS are on disk either way - only the .tsx was not written.`);
94
+ return;
95
+ }
79
96
  for (const file of files) {
80
97
  await writeFile(join(compDir, file.filename), file.code, "utf8");
81
98
  }
@@ -8,7 +8,7 @@ import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalle
8
8
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
9
9
  import { flavourResolver } from "../styles-flavour.js";
10
10
  import { projectTongue } from "../their-tongue.js";
11
- import { recordWritten } from "../written.js";
11
+ import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
12
12
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
13
13
  * a folder holding only refit artifacts doesn't count). */
14
14
  /** True when the system is actually installed (tokens.css present). */
@@ -122,6 +122,26 @@ export async function refit(file, opts) {
122
122
  const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
123
123
  /** O VOCABULÁRIO DELE - o `refit` reescreve o componente e não passava pela porta. */
124
124
  await projectTongue(root, slug), await installedThemeVars(root, slug));
125
+ /**
126
+ * A EDICAO DELE VENCE A REESCRITA (T7) - a leitura que faltava, a mesma do `generate`.
127
+ *
128
+ * Este comando REESCREVE um componente que a pessoa ja' tem, que e' exatamente a situacao em
129
+ * que o arquivo dela mais provavelmente foi tocado. Ele gravava o fingerprint e nunca o lia.
130
+ */
131
+ const edited = await theirEdits(join(root, "_synthesisui", "ds", slug), res.name, compDir);
132
+ if (edited && edited.length > 0 && !opts.force) {
133
+ console.log("");
134
+ console.log(
135
+ /**
136
+ * O COMANDO QUE RETOMA E' O QUE ELA ACABOU DE RODAR - `refit` recebe o CAMINHO do
137
+ * arquivo-fonte, nunca o nome do componente. Sugerir `refit ${res.name}` devolveria
138
+ * `Could not read "<nome>"`, e uma saida que nao funciona e' pior que nenhuma saida:
139
+ * a pessoa fez o que a tela mandou e levou uma segunda recusa.
140
+ */
141
+ keptLine(res.name, edited, `npx synthesisui refit ${file}${opts.name ? ` --name ${opts.name}` : ""} --force`));
142
+ console.log(` The recipe is saved in your system's draft either way - only the .tsx was not written.`);
143
+ return;
144
+ }
125
145
  for (const f of files) {
126
146
  await writeFile(join(compDir, f.filename), f.code, "utf8");
127
147
  }
@@ -582,6 +582,24 @@ export async function remeasure(args) {
582
582
  console.log(body(`${out.written ?? 0} of ${out.total ?? 0} components written.`));
583
583
  for (const note of out.notes ?? [])
584
584
  console.log(body(paint.faint(note)));
585
+ /**
586
+ * A RECUSA, DITA - e este era o unico numero da tela sem explicacao.
587
+ *
588
+ * O QUE O CLIENTE VIA (medido em 10/09): `60 of 62 components written.` e mais nada. Os dois
589
+ * que faltaram estavam nomeados na resposta, com o motivo, e o comando descartava o campo. Quem
590
+ * le "60 de 62" so' pode concluir uma coisa - que a esteira perdeu dois - e vai procurar um
591
+ * defeito que nao existe, ou pior, nao vai.
592
+ *
593
+ * A COPY JA' EXISTIA: `because` e' a frase do servidor, e reescreve-la aqui seria a segunda
594
+ * versao da mesma explicacao. A primeira coisa que a segunda faz e' discordar da primeira.
595
+ */
596
+ const refused = out.refused ?? [];
597
+ if (refused.length > 0) {
598
+ console.log("");
599
+ console.log(body(`${refused.length} ${refused.length === 1 ? "component was" : "components were"} refused - ${refused.length === 1 ? "it is" : "they are"} not lost, ${refused.length === 1 ? "it was" : "they were"} not written:`));
600
+ for (const r of refused)
601
+ console.log(body(paint.strong(` ✕ ${r.name} - ${r.because}`)));
602
+ }
585
603
  /**
586
604
  * A RECUSA POR ESCOPO, DITA - e não como um número que some.
587
605
  *
@@ -1,8 +1,9 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
2
+ import { dirname, join, relative } from "node:path";
3
3
  import { readProjectConfig, resolveRegistry } from "../config.js";
4
4
  import { fetchTemplate } from "../registry.js";
5
5
  import { inTheirTongue, projectTongue } from "../their-tongue.js";
6
+ import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
6
7
  /**
7
8
  * Materializes a whole page from a DS template into the project (hybrid
8
9
  * codegen-first): the server codegens deterministic files, we write them, and
@@ -28,6 +29,36 @@ export async function template(slug, name, opts) {
28
29
  const defaultDir = join("templates", asName ?? name);
29
30
  const pageRel = opts.out ?? join(defaultDir, pageFile.filename);
30
31
  const pageDir = dirname(join(root, pageRel));
32
+ /**
33
+ * A EDICAO DELE VENCE A REESCRITA (T7) - a MESMA guarda do `component`, no comando que a nao
34
+ * tinha.
35
+ *
36
+ * MEDIDO EM 10/09: `component.ts` consultava o registro do que nos escrevemos em seis pontos e
37
+ * `template.ts` em nenhum. E este comando escreve MAIS: uma pagina inteira, mais a folha
38
+ * co-locada. Alguem que traz um template, ajusta o texto, e roda de novo para pegar uma correcao
39
+ * perdia o ajuste - sem aviso, sem `--force`, sem uma frase.
40
+ *
41
+ * A MESMA CHAVE PARA OS DOIS LADOS: `templates/<nome>` e' o que a leitura procura e o que a
42
+ * escrita grava, entao o fingerprint de hoje e' lido pela rodada de amanha. Sem `.lock`
43
+ * (template trazido antes do install) nao ha' memoria, e o comportamento de sempre continua.
44
+ */
45
+ const slugDir = join(root, "_synthesisui", "ds", slug);
46
+ const entry = relative(root, pageDir).split(/[\\/]/).join("/");
47
+ const editedHere = await theirEdits(slugDir, entry, pageDir);
48
+ if (editedHere && editedHere.length > 0 && !opts.force) {
49
+ console.log("");
50
+ console.log(keptLine(entry, editedHere,
51
+ /**
52
+ * O RETAKE CARREGA `--out`, senao ele escreve em OUTRO LUGAR.
53
+ *
54
+ * Sem esta parte, quem rodou `template landing --out app/page.tsx` e seguiu a instrucao
55
+ * da recusa escreveria em `templates/landing/page.tsx`: o arquivo que ele editou fica
56
+ * intocado, nasce um segundo, e nada diz que aconteceu. Uma saida que escreve no lugar
57
+ * errado e' pior que uma recusa sem saida.
58
+ */
59
+ `npx synthesisui template ${slug} ${name}${asName ? ` --as ${asName}` : ""}${opts.out ? ` --out ${opts.out}` : ""} --force`));
60
+ return;
61
+ }
31
62
  /**
32
63
  * NENHUMA MATERIALIZAÇÃO VAZA VOCABULÁRIO INTERNO (INV-VOLTA-02) - a mesma porta do
33
64
  * `component`. Uma página inteira saía com `var(--ds-*)` cru enquanto um componente avulso
@@ -40,6 +71,8 @@ export async function template(slug, name, opts) {
40
71
  let inlined = 0;
41
72
  const still = new Set();
42
73
  await mkdir(pageDir, { recursive: true });
74
+ /** OS BYTES QUE FORAM A DISCO, para o fingerprint lembrar EXATAMENTE o que escrevemos. */
75
+ const landed = [];
43
76
  const spokenPage = speak(pageFile.code);
44
77
  if (spokenPage) {
45
78
  named += spokenPage.named;
@@ -47,7 +80,12 @@ export async function template(slug, name, opts) {
47
80
  for (const l of spokenPage.left)
48
81
  still.add(l);
49
82
  }
50
- await writeFile(join(root, pageRel), spokenPage ? spokenPage.css : pageFile.code, "utf8");
83
+ const pageContent = spokenPage ? spokenPage.css : pageFile.code;
84
+ await writeFile(join(root, pageRel), pageContent, "utf8");
85
+ landed.push({
86
+ filename: pageRel.split(/[\\/]/).pop() ?? pageFile.filename,
87
+ content: pageContent,
88
+ });
51
89
  console.log(`✓ wrote ${pageRel} (${slug} v${generated.version})`);
52
90
  for (const f of siblings) {
53
91
  const rel = join(dirname(pageRel), f.filename);
@@ -58,9 +96,19 @@ export async function template(slug, name, opts) {
58
96
  for (const l of spoken.left)
59
97
  still.add(l);
60
98
  }
61
- await writeFile(join(root, rel), spoken ? spoken.css : f.code, "utf8");
99
+ const content = spoken ? spoken.css : f.code;
100
+ await writeFile(join(root, rel), content, "utf8");
101
+ landed.push({ filename: f.filename, content });
62
102
  console.log(`✓ wrote ${rel}`);
63
103
  }
104
+ /**
105
+ * E O REGISTRO E' GRAVADO - a outra metade da guarda, e sem ela a primeira nunca dispara.
106
+ *
107
+ * A leitura la' em cima procura o fingerprint desta escrita. Ler sem gravar seria uma guarda que
108
+ * passa em todo teste e nunca protege ninguem: a promessa so' existe quando as duas pontas
109
+ * fecham.
110
+ */
111
+ await recordWritten(slugDir, entry, landed);
64
112
  if (named > 0 || inlined > 0) {
65
113
  console.log(` ${named} reference${named === 1 ? "" : "s"} now speak${named === 1 ? "s" : ""} the name YOUR code gives the value${inlined > 0 ? `, and ${inlined} carr${inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
66
114
  if (still.size > 0) {
@@ -211,6 +211,56 @@ async function theOnlyInstalled(root) {
211
211
  throw new RegistryError("No design system is installed here - run `synthesisui add <slug>` first.");
212
212
  throw new RegistryError(`More than one system is installed here (${slugs.join(", ")}) - name the one to update: \`synthesisui upgrade <slug>\`.`);
213
213
  }
214
+ /**
215
+ * O QUE ELE EDITOU DEPOIS DE NOS ESCREVERMOS - a mesma leitura, num lugar so'.
216
+ *
217
+ * A PROMESSA T7 e' *"kept YOUR file(s)"*, e ela vivia dentro do laco que regenera. Isso amarrou a
218
+ * promessa a UM caminho: o do upgrade que tem versao nova. Medido em 10/09 - com a versao igual,
219
+ * `upgrade` retornava antes de chegar ao laco, e a pessoa que editou tres componentes nao ouvia
220
+ * nada sobre eles no comando cuja funcao e' justamente dizer o que aconteceu com os arquivos dela.
221
+ *
222
+ * Extraida, a resposta e' a mesma nos tres caminhos - rematerializou, ja' estava na ultima, ou
223
+ * subiu de versao. Uma leitura, uma frase.
224
+ */
225
+ async function editedByHand(root, slug, slugDir, componentsDir) {
226
+ const componentsRoot = join(root, componentsDir);
227
+ const marker = `from the "${slug}" design system`;
228
+ const writtenMap = await readWritten(slugDir);
229
+ const out = [];
230
+ for (const entry of await readdir(componentsRoot).catch(() => [])) {
231
+ let head = "";
232
+ try {
233
+ head = (await readFile(join(componentsRoot, entry, `${entry}.tsx`), "utf8")).slice(0, 300);
234
+ }
235
+ catch {
236
+ continue; // not a materialized component folder
237
+ }
238
+ if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
239
+ continue;
240
+ const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
241
+ if (edited && edited.length > 0)
242
+ out.push({ entry, edited });
243
+ }
244
+ return out;
245
+ }
246
+ /**
247
+ * E A FRASE, DITA - nos caminhos que NAO regeneram nada.
248
+ *
249
+ * Aqui ela nao e' o relatorio de uma reescrita que respeitou o arquivo dele: e' a confirmacao de
250
+ * que este comando passou e nao tocou no que e' dele. E' a mesma informacao, e omiti-la nos
251
+ * caminhos silenciosos e' o que fazia a promessa valer so' as vezes.
252
+ */
253
+ async function sayWhatWasKept(root, slug, slugDir) {
254
+ const config = await readProjectConfig(root).catch(() => null);
255
+ if (!config || config.target !== "next")
256
+ return;
257
+ const kept = await editedByHand(root, slug, slugDir, config.componentsDir);
258
+ if (kept.length === 0)
259
+ return;
260
+ console.log("");
261
+ for (const k of kept)
262
+ console.log(body(keptLine(k.entry, k.edited, `npx synthesisui component ${slug} ${k.entry}`)));
263
+ }
214
264
  export async function upgrade(asked, opts) {
215
265
  const base = resolveRegistry(opts.registry);
216
266
  const root = opts.dir ?? process.cwd();
@@ -263,6 +313,8 @@ export async function upgrade(asked, opts) {
263
313
  * medido.
264
314
  */
265
315
  const measured = await checkAfterWriting(root, `v${installed}`);
316
+ /** ESTE CAMINHO REESCREVE OS ARTEFATOS - e o que e' dele fica onde esta'. Ver `sayWhatWasKept`. */
317
+ await sayWhatWasKept(root, slug, slugDir);
266
318
  await reportWhatIsLeft(root, {
267
319
  ...(opts.cli ? { cli: opts.cli } : {}),
268
320
  ...(measured > 0 ? { justMeasured: measured } : {}),
@@ -271,6 +323,12 @@ export async function upgrade(asked, opts) {
271
323
  }
272
324
  if (!opts.force) {
273
325
  console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
326
+ /**
327
+ * E AQUI TAMBEM, que e' o caminho mais percorrido de todos: sem gap de versao, este comando
328
+ * dizia uma linha e sumia. Quem editou tres componentes ficava sem saber se eles seguem sendo
329
+ * dele - no comando que existe para responder isso.
330
+ */
331
+ await sayWhatWasKept(root, slug, slugDir);
274
332
  await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
275
333
  return;
276
334
  }
@@ -308,8 +366,9 @@ export async function upgrade(asked, opts) {
308
366
  const regenerated = [];
309
367
  const failed = [];
310
368
  /** Componentes que ELE editou desde que os escrevemos - mantidos intactos (T7). */
311
- const kept = [];
312
- const writtenMap = await readWritten(slugDir);
369
+ const kept = await editedByHand(root, slug, slugDir, config.componentsDir);
370
+ /** Os que ELE editou saem da regeneracao - a leitura acima ja' os separou. */
371
+ const keptEntries = new Set(kept.map((k) => k.entry));
313
372
  if (config.target === "next") {
314
373
  const componentsRoot = join(root, config.componentsDir);
315
374
  const marker = `from the "${slug}" design system`;
@@ -345,16 +404,13 @@ export async function upgrade(asked, opts) {
345
404
  if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
346
405
  continue;
347
406
  /**
348
- * A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `written.ts`. O fingerprint do que NÓS
407
+ * A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `editedByHand`. O fingerprint do que NÓS
349
408
  * escrevemos foi gravado no `.lock` na hora da escrita; se o disco divergiu, ele editou, e o
350
409
  * arquivo é DELE - fica intacto e o terminal diz qual. `null` é install de antes do
351
410
  * fingerprint: sem memória não há veredito, e o comportamento de sempre continua.
352
411
  */
353
- const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
354
- if (edited && edited.length > 0) {
355
- kept.push({ entry, edited });
412
+ if (keptEntries.has(entry))
356
413
  continue;
357
- }
358
414
  try {
359
415
  const res = await fetchComponent(base, slug, entry);
360
416
  const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
@@ -460,7 +516,7 @@ export async function upgrade(asked, opts) {
460
516
  if (kept.length > 0) {
461
517
  console.log("");
462
518
  for (const k of kept)
463
- console.log(body(keptLine(slug, k.entry, k.edited)));
519
+ console.log(body(keptLine(k.entry, k.edited, `npx synthesisui component ${slug} ${k.entry}`)));
464
520
  }
465
521
  const measured = await checkAfterWriting(root, `v${latest.version}`);
466
522
  console.log(section("Migrate the app"));
package/dist/index.js CHANGED
@@ -117,6 +117,7 @@ Options:
117
117
  --instruction <s> refit: extra guidance for the adaptation
118
118
  --dry refit: adapt and print, but save nothing
119
119
  --force clean: apply the changes (without it, dry run)
120
+ component/template/generate/refit: overwrite a file YOU edited
120
121
  upgrade: rewrite UPGRADE.md even with no version gap left
121
122
  --from <n> upgrade --force: which older snapshot to diff from
122
123
  --strict doctor: exit 1 when drift is found in THIS repo (for CI)
@@ -155,6 +156,21 @@ Examples:
155
156
  async function main() {
156
157
  const { positionals, flags } = parseFlags(process.argv.slice(2));
157
158
  const [command, ...args] = positionals;
159
+ /**
160
+ * `synthesisui --version` RESPONDE A VERSAO - uma linha, exit 0.
161
+ *
162
+ * O QUE ACONTECIA (medido em 10/09): `node dist/index.js --version` imprimia as 117 linhas do
163
+ * help e saia 0. Quem pergunta a versao de uma ferramenta - uma pessoa num terminal, um script
164
+ * de CI, um relatorio de bug - recebia a lista de comandos, e a unica forma de descobrir a
165
+ * versao era abrir o `package.json` da instalacao.
166
+ *
167
+ * SEM COMANDO, sempre: `--version <n>` continua sendo flag do `add`, do `component` e do
168
+ * `template`, e la ela chega com um comando na frente. A distincao e' posicional, nao de nome.
169
+ */
170
+ if (!command && flags.version !== undefined) {
171
+ console.log(CLI_VERSION);
172
+ return;
173
+ }
158
174
  if (!command || flags.help || command === "help") {
159
175
  /**
160
176
  * A PRIMEIRA LINHA FALA DO PROJETO ONDE ELA FOI RODADA - ver `where-you-are.ts`.
@@ -484,6 +500,8 @@ async function main() {
484
500
  target,
485
501
  version,
486
502
  as: as_,
503
+ /** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
504
+ force: flags.force === true,
487
505
  });
488
506
  break;
489
507
  }
@@ -579,6 +597,8 @@ async function main() {
579
597
  instruction: typeof flags.instruction === "string" ? flags.instruction : undefined,
580
598
  support: typeof flags.support === "string" ? flags.support : undefined,
581
599
  dry: flags.dry === true,
600
+ /** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
601
+ force: flags.force === true,
582
602
  });
583
603
  break;
584
604
  }
@@ -681,7 +701,14 @@ async function main() {
681
701
  }
682
702
  const ds = typeof flags.ds === "string" ? flags.ds : undefined;
683
703
  const name = typeof flags.name === "string" ? flags.name : undefined;
684
- await generate(description, { registry, dir, ds, name });
704
+ await generate(description, {
705
+ registry,
706
+ dir,
707
+ ds,
708
+ name,
709
+ /** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
710
+ force: flags.force === true,
711
+ });
685
712
  break;
686
713
  }
687
714
  default:
@@ -170,7 +170,24 @@
170
170
  * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
171
171
  * sistema, em vez de só receber as regras e adivinhar o resto.
172
172
  */
173
- export const MATERIALISER_SINCE = "0.16.403";
173
+ /**
174
+ * 0.16.403 -> 0.16.412 em 10/09, e o SIM é sobre TRÊS coisas que caem na pasta dele:
175
+ *
176
+ * `component` sem `add` sem `.lock`, o vocabulário do repositório dele passa a ser derivado
177
+ * em memória do sistema lido no registry. O `.tsx` que o CLI de ontem
178
+ * escrevia com `var(--ds-*)` cru sai com o nome que o código DELE dá
179
+ * àquele valor - e a nossa folha deixa de ser pré-requisito para
180
+ * trazer um componente.
181
+ * `template` com T7 a página materializada grava o fingerprint no `.lock` e passa a
182
+ * recusar a reescrita do que ele editou à mão. Campo novo no arquivo
183
+ * que o time dele commita.
184
+ * `generate` e `refit` gravavam o fingerprint e nunca o liam; agora recusam por edição
185
+ * dele, então o conjunto de bytes que uma rodada deixa é outro.
186
+ *
187
+ * O que o cliente ganha ao rodar `upgrade`: os componentes que ele já tem voltam a ser reescritos
188
+ * pelo materializador que respeita a edição dele em TODO comando, e não só em dois.
189
+ */
190
+ export const MATERIALISER_SINCE = "0.16.412";
174
191
  /**
175
192
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
176
193
  *
@@ -0,0 +1,78 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * ESTA PASTA E' UM PROJETO? - a pergunta que todo comando que ESCREVE no repositorio dele
6
+ * deveria fazer antes da primeira escrita.
7
+ *
8
+ * O QUE O CLIENTE VIVIA: `connect` rodado por engano no `~` ou num diretorio recem-criado
9
+ * escrevia `.claude/settings.json`, `CLAUDE.md`, `.mcp.json` e a pasta de governanca ali mesmo -
10
+ * seis escritas, medidas em 10/09, nenhuma delas precedida de uma checagem. Ninguem percebe no
11
+ * dia; percebe semanas depois, quando um `CLAUDE.md` orfao na home passa a instruir todo agente
12
+ * que abre naquela pasta.
13
+ *
14
+ * O CRITERIO E' O DO ECOSSISTEMA, nao um nosso: um `package.json` nesta pasta ou acima dela. A
15
+ * subida para no diretorio que carrega o `.git`, porque a raiz do repositorio e' onde o projeto
16
+ * acaba - continuar subindo alcancaria a home de quem roda, que e' exatamente o acidente que isto
17
+ * existe para recusar.
18
+ *
19
+ * GENERALIDADE: nao pergunta por Next, por React nem pela nossa pasta. Um projeto que ainda nao
20
+ * instalou nada e' um projeto; uma pasta vazia dentro de um repositorio Node tambem e', porque o
21
+ * manifesto de cima responde por ela.
22
+ */
23
+ export async function projectRootFrom(dir) {
24
+ /**
25
+ * DENTRO DE UMA DEPENDENCIA NAO E' O PROJETO DELE - e' o projeto de outra pessoa, baixado.
26
+ *
27
+ * Um `cd` de distancia do acidente: `node_modules/<lib>` tem `package.json`, entao a subida
28
+ * parava ali e o comando escrevia a fiacao do agente dentro de uma dependencia - que some no
29
+ * proximo `npm ci`, levando junto tudo que ele configurou.
30
+ */
31
+ if (dir.split(/[\\/]/).includes("node_modules"))
32
+ return null;
33
+ /**
34
+ * E A HOME E' O SEGUNDO TETO, ao lado da raiz do repositorio.
35
+ *
36
+ * O `.git` sozinho nao fechava o acidente que este modulo existe para recusar: numa maquina
37
+ * onde alguem rodou `npm init` na home - acontece -, uma pasta solta em `~/qualquer-coisa` sem
38
+ * git subia ate' `~`, encontrava aquele manifesto, e o comando escrevia `CLAUDE.md`,
39
+ * `.mcp.json` e a casa da governanca na HOME da pessoa.
40
+ *
41
+ * A home nunca e' "o projeto dele", tenha ela manifesto ou nao.
42
+ */
43
+ const home = homedir();
44
+ let at = dir;
45
+ for (;;) {
46
+ if (at === home)
47
+ return null;
48
+ if (await exists(join(at, "package.json")))
49
+ return at;
50
+ /** A raiz do repositorio e' o teto: acima dela nao existe "o projeto dele". */
51
+ if (await exists(join(at, ".git")))
52
+ return null;
53
+ const up = dirname(at);
54
+ if (up === at)
55
+ return null;
56
+ at = up;
57
+ }
58
+ }
59
+ async function exists(path) {
60
+ return stat(path).then(() => true, () => false);
61
+ }
62
+ /**
63
+ * A RECUSA, COM O MOTIVO E O QUE FAZER - nunca so' "nao posso".
64
+ *
65
+ * E ELA DIZ O QUE MEDIMOS, nao o que a pasta e'. A primeira redacao decretava
66
+ * *"<dir> is not a project"* e oferecia `npm init -y`: num repositorio Deno ou Rust a primeira
67
+ * frase e' falsa - aquilo E' um projeto - e a segunda manda sujar o repositorio dele com o
68
+ * manifesto de um ecossistema que ele nao usa. Representar o limite do leitor e' diferente de
69
+ * negar o que ele tem.
70
+ */
71
+ export function notAProject(command, dir) {
72
+ return [
73
+ `No package.json in ${dir}, or in any folder above it up to the repository root - so this reader cannot tell which project it is looking at.`,
74
+ `\`${command}\` writes agent files into a project - a hook, an MCP entry, CLAUDE.md - so it stops before writing anything.`,
75
+ `Point it at the project: npx synthesisui ${command} --dir <path/to/project>`,
76
+ `(it looks for a package.json - that is what this reader knows how to place files in)`,
77
+ ];
78
+ }
@@ -50,6 +50,33 @@ export function inTheirTongue(css, tongue, destination = "stylesheet") {
50
50
  });
51
51
  return { css: out, named, inlined, left: [...left].sort() };
52
52
  }
53
+ /**
54
+ * O VOCABULARIO DESTE REPOSITORIO **SEM O `add` TER RODADO** - a folha deixa de ser pre-requisito.
55
+ *
56
+ * O QUE ISTO MUDA PARA O CLIENTE: ele pede UM componente e recebe um arquivo que ja' fala a
57
+ * lingua do codigo dele - `var(--color-ink-900)`, o nome que ELE deu - sem antes materializar as
58
+ * 1813 linhas, 111 variaveis e 628 referencias que o `add` escreve no repositorio (medido em
59
+ * 10/09). A nossa folha e' a REFERENCIA que o agente dele le'; exigi-la para trazer um botao
60
+ * invertia isso, e contrariava a definicao de produto: nada nosso no repositorio dele.
61
+ *
62
+ * A MEDICAO E' A MESMA DO `add`, so' que em memoria: `pointTokensAtTheirNames` varre as folhas
63
+ * DELE e o build dele, e responde quais das nossas variaveis tem nome dele. A diferenca e' que
64
+ * nada disso vai para o disco - nem o mapa, nem a folha.
65
+ *
66
+ * `null` quando o repositorio dele nao nomeia nada do que este sistema declara. Ai' a folha
67
+ * continua sendo o caminho, e quem chama DIZ isso - em vez de escrever `var(--ds-*)` cru e
68
+ * deixar a pessoa descobrir por que a cor nao apareceu.
69
+ */
70
+ export async function tongueFromArtifacts(root, artifacts) {
71
+ const tokens = artifacts["tokens.css"] ?? "";
72
+ if (!tokens)
73
+ return null;
74
+ const { pointTokensAtTheirNames } = await import("./their-vars.js");
75
+ const { pairs } = await pointTokensAtTheirNames(root, { artifacts });
76
+ if (pairs.length === 0)
77
+ return null;
78
+ return tongueOf(pairs, tokens);
79
+ }
53
80
  /**
54
81
  * O VOCABULÁRIO DESTE REPOSITÓRIO, montado do que o `add` já deixou na pasta.
55
82
  *
@@ -36,6 +36,25 @@ import { detectAppDirs, sheetChainOf } from "./global-sheet.js";
36
36
  *
37
37
  * The system itself was always resolved from the root. So is this now.
38
38
  */
39
+ /**
40
+ * O QUE NOS ESCREVEMOS NAO E' FIACAO DELE - e isto era um FALSO SILENCIO, o pior dos dois.
41
+ *
42
+ * MEDIDO EM 10/09: o cabecalho que `component-codegen.ts` poe em todo `.tsx` materializado diz,
43
+ * em comentario, *"Global setup (once per app): import _synthesisui/ds/<slug>/tokens.css"* e
44
+ * *"put data-ds=<slug> on a root element"*. A varredura abaixo procura exatamente essas duas
45
+ * strings em QUALQUER arquivo do repositorio - entao, a partir do primeiro componente que nos
46
+ * mesmos escrevemos, ela passava a responder `imported: true` e `scoped: true` sobre um projeto
47
+ * que nao importa nada e nao tem escopo nenhum.
48
+ *
49
+ * O QUE O CLIENTE VIVIA: ele traz um componente, a tela diz *"This project already imports
50
+ * <slug>'s tokens.css and carries data-ds - nothing to set up"*, ele cola o componente na pagina,
51
+ * e ve' um bloco sem cor, sem tipografia e sem sombra. O comando acabou de garantir que estava
52
+ * tudo certo. O `doctor` le' pela mesma porta, entao dizia o mesmo.
53
+ *
54
+ * A instrucao que ESTE arquivo carrega e' uma instrucao, nao um cumprimento dela. Um comentario
55
+ * nosso nunca prova nada sobre o projeto dele.
56
+ */
57
+ const OURS = "Generated by SynthesisUI";
39
58
  export async function readWiring(root, slug) {
40
59
  const w = {
41
60
  imported: false,
@@ -84,6 +103,9 @@ export async function readWiring(root, slug) {
84
103
  const src = await readFile(file, "utf8").catch(() => "");
85
104
  if (!src)
86
105
  continue;
106
+ /** Ver `OURS`: o nosso proprio cabecalho respondia por ele. */
107
+ if (src.slice(0, 300).includes(OURS))
108
+ continue;
87
109
  if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
88
110
  w.imported = true;
89
111
  if (src.includes(`_synthesisui/ds/${slug}/theme.css`))
package/dist/written.js CHANGED
@@ -60,11 +60,33 @@ export async function editedSinceWritten(recorded, dir) {
60
60
  return edited;
61
61
  }
62
62
  /**
63
- * A FRASE DO TERMINAL, uma por componente mantido - o critério do T7 é a prova VISÍVEL: o dono
64
- * fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato. A
65
- * saída oferecida regenera por ESCOLHA (o `component` reescreve e regrava o fingerprint).
63
+ * A PERGUNTA, NUM LUGAR SO': *ele editou o que escrevemos aqui?*
64
+ *
65
+ * As duas linhas - ler o registro do `.lock`, comparar com o disco - estavam copiadas em cada
66
+ * comando que se lembrou de perguntar, e ausentes nos que nao se lembraram. Medido em 10/09:
67
+ * `component` 6 guardas, `upgrade` 4, `generate` e `refit` gravavam o fingerprint e nunca o liam,
68
+ * `template` nao fazia nem uma coisa nem outra.
69
+ *
70
+ * Uma porta so' torna a omissao visivel: um comando de materializacao que nao a chama aparece no
71
+ * portao de `every-writer-keeps-his-edits.spec.ts`, em vez de aparecer no repositorio de alguem.
72
+ *
73
+ * `null` e' install legado (sem fingerprint gravado) - sem memoria nao ha' veredito, e quem chama
74
+ * mantem o comportamento de sempre.
75
+ */
76
+ export async function editedHere(slugDir, entry, dir) {
77
+ return editedSinceWritten((await readWritten(slugDir))[entry], dir);
78
+ }
79
+ /**
80
+ * A FRASE DO TERMINAL, uma por arquivo mantido - o critério do T7 é a prova VISÍVEL: o dono
81
+ * fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato.
82
+ *
83
+ * `retake` E' OBRIGATORIO, e é o comando que RETOMA a versão nova. Ele era fixo em
84
+ * `component <slug> <entry>`, o que estava certo enquanto só um comando falava esta frase: no
85
+ * `template` aquela linha mandaria a pessoa rodar o comando errado, e uma saída que não funciona
86
+ * é pior que nenhuma. Obrigatório em vez de opcional porque um default aqui volta a ser o
87
+ * comando de outro comando, calado.
66
88
  */
67
- export function keptLine(slug, entry, edited) {
89
+ export function keptLine(entry, edited, retake) {
68
90
  return (`! ${entry}: you edited ${edited.join(", ")} after we wrote it - kept YOUR file(s).\n` +
69
- ` To take the new version anyway: npx synthesisui component ${slug} ${entry}`);
91
+ ` To take the new version anyway: ${retake}`);
70
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.411",
3
+ "version": "0.16.412",
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": {