synthesisui 0.16.429 → 0.16.430

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.
@@ -47,6 +47,12 @@ const IGNORED = [
47
47
  /** O que a última medição viu, para a próxima dizer o que mudou - descreve ESTE clone. */
48
48
  ".last-sync.json",
49
49
  "not-expressed.md",
50
+ /**
51
+ * O RELATÓRIO QUE O `inspect` ESCREVE - a leitura inteira com forma, para ele abrir no navegador.
52
+ * Mesma natureza do censo: é a MEDIÇÃO, reescrita por inteiro a cada rodada, então versioná-la é
53
+ * conflito toda vez e ainda poria um HTML de meio megabyte no diff de quem só rodou o comando.
54
+ */
55
+ "report.html",
50
56
  /**
51
57
  * O MODO DELA - ver `mode.ts`. Commitado, ele barraria o time inteiro porque UMA pessoa ligou
52
58
  * o bloqueio dela, e o aceite da etapa 06 é literal: *"a escolha de uma pessoa nunca barra
@@ -78,7 +84,12 @@ export async function ensureGovernanceHome(projectRoot) {
78
84
  await mkdir(join(projectRoot, "_synthesisui"), { recursive: true });
79
85
  await writeGovernanceIgnore(projectRoot);
80
86
  }
81
- async function writeGovernanceIgnore(projectRoot) {
87
+ /**
88
+ * EXPORTADO desde 18/09 porque o `inspect` passou a escrever um arquivo na pasta: o relatório. Um
89
+ * comando de primeiro contato que deixa um HTML de 30 KB aparecendo no `git status` de quem só quis
90
+ * olhar cobra da pessoa uma limpeza que ela não pediu - e a pasta já é declarada gerenciada aqui.
91
+ */
92
+ export async function writeGovernanceIgnore(projectRoot) {
82
93
  const path = join(projectRoot, "_synthesisui", ".gitignore");
83
94
  const existing = await readFile(path, "utf8").catch(() => null);
84
95
  /**
@@ -55,12 +55,14 @@ import { body, bodyWrapped, paint, section } from "../output.js";
55
55
  import { consumersOf, outsideScope, } from "../outside-scope.js";
56
56
  import { phase, startProgress } from "../progress.js";
57
57
  import { repoStateOf } from "../repo-state.js";
58
+ import { relatorioHtml, secoesDe } from "../report-html.js";
58
59
  import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
59
60
  import { detectStack, resolveDeps, stackVersions } from "../stack.js";
61
+ import { desligarCaptura, ligarCaptura } from "../transcript.js";
60
62
  import { wiredSlugs, wiringWarning } from "../wired-slugs.js";
61
63
  import { harvestWorkspaceCss } from "../workspace-css.js";
62
64
  import { placeInWorkspace } from "../workspace-place.js";
63
- import { add } from "./add.js";
65
+ import { add, writeGovernanceIgnore } from "./add.js";
64
66
  import { walk, walkAll } from "./doctor.js";
65
67
  /**
66
68
  * UMA PÁGINA DELE, como ele a montou - ver `Census.pages`.
@@ -3720,6 +3722,145 @@ export function defaultScheme(declared) {
3720
3722
  return dark > light ? "dark" : "light";
3721
3723
  }
3722
3724
  /** On a dry run there is nobody to ask, so say what was measured. */
3725
+ /**
3726
+ * A TELA DO `inspect`, E O ARQUIVO QUE CARREGA O RESTO (dono, 18/09).
3727
+ *
3728
+ * O QUE O CLIENTE GANHA: ele decide sem rolar. Ficam na tela os quatro blocos que MUDAM o que ele
3729
+ * vai fazer em seguida - quanto foi lido, se o root tem mais de um app, o que não resolve, e onde
3730
+ * está a leitura inteira. Medido na saída dele em 18/09: 250 linhas, 94 delas a mesma frase sobre
3731
+ * nome de parte. Nada se perde: o texto inteiro está no relatório, escrito pela MESMA esteira.
3732
+ *
3733
+ * A FRASE DO CONTRATO MUDA, E É A ÚNICA COISA QUE MUDA: era "nothing was written, and nothing was
3734
+ * sent". Passa a dizer a verdade - nada enviado, um arquivo escrito, e aqui está ele. Uma frase que
3735
+ * promete silêncio enquanto o comando escreve um arquivo é pior que o arquivo.
3736
+ */
3737
+ async function mostrarInspecao(c, root, transcrito) {
3738
+ const secoes = secoesDe(transcrito);
3739
+ const valores = c.values?.classes;
3740
+ const decisoes = valores ? valores.seen - valores.structure : 0;
3741
+ const pct = valores && decisoes > 0
3742
+ ? Math.round((valores.interpreted / decisoes) * 100)
3743
+ : null;
3744
+ const lidos = c.coverage?.read ?? 0;
3745
+ const total = c.coverage?.components ?? 0;
3746
+ const arquivo = join(root, "_synthesisui", "report.html");
3747
+ const html = () => relatorioHtml({
3748
+ cabecalho: {
3749
+ repo: c.measured?.repo ?? basename(root),
3750
+ medidoEm: c.measured?.at ?? "",
3751
+ cli: c.ledger?.cli ?? "",
3752
+ },
3753
+ destaques: [
3754
+ {
3755
+ numero: `${lidos} of ${total}`,
3756
+ rotulo: "components came out with a blueprint",
3757
+ },
3758
+ ...(pct === null
3759
+ ? []
3760
+ : [
3761
+ {
3762
+ numero: `${pct}%`,
3763
+ rotulo: `of ${decisoes} style decisions carry your vocabulary`,
3764
+ },
3765
+ ]),
3766
+ {
3767
+ numero: String(c.brokenRefs?.length ?? 0),
3768
+ rotulo: "references that resolve to nothing",
3769
+ },
3770
+ ],
3771
+ secoes,
3772
+ });
3773
+ console.log(section("What came out of your files"));
3774
+ console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
3775
+ ? ""
3776
+ : `, and ${paint.strong(`${pct}%`)} of your ${decisoes} style decisions carry your own vocabulary`}.`));
3777
+ /**
3778
+ * OS DOIS BLOCOS QUE MUDAM O QUE ELE FAZ EM SEGUIDA, e eles saem do transcrito em vez de serem
3779
+ * reescritos: a régua que os produziu é a mesma, então a tela nunca diverge do relatório.
3780
+ */
3781
+ for (const titulo of DECIDEM) {
3782
+ const achada = secoes.find((s) => s.titulo === titulo);
3783
+ if (!achada)
3784
+ continue;
3785
+ console.log(section(titulo));
3786
+ for (const linha of achada.linhas)
3787
+ console.log(linha);
3788
+ }
3789
+ /**
3790
+ * O ARQUIVO SÓ NASCE SE ELE PEDIR (dono, 18/09) - e é isto que mantém `INV-INSPECT-01` de pé.
3791
+ *
3792
+ * A promessa que faz alguém rodar este comando sem medo é que nada acontece: ele lê, mostra, e
3793
+ * o repositório continua exatamente como estava. Escrever o relatório por conta própria
3794
+ * quebraria isso para TODO mundo, inclusive para quem só queria olhar - e num primeiro contato
3795
+ * essa é a confiança inteira. Perguntando, a escrita deixa de ser nossa e passa a ser dele.
3796
+ *
3797
+ * SEM TERMINAL, NÃO PERGUNTA E NÃO ESCREVE: um `inspect` dentro de um pipe, de um CI ou de um
3798
+ * agente não tem quem responda, e um default que escreve seria a mesma quebra por outra porta.
3799
+ * É a mesma guarda que `askName` já usa.
3800
+ */
3801
+ console.log(section("The whole reading, in one file"));
3802
+ console.log(body(`${secoes.length} sections did not print here - every value, every fragment, every part.`));
3803
+ console.log(body(paint.faint("A single self-contained page in your project. Nothing is sent either way.")));
3804
+ if (!(await quer(` write the full report? [Y/n]: `))) {
3805
+ console.log("");
3806
+ console.log(body("Nothing was written, and nothing was sent."));
3807
+ console.log("");
3808
+ console.log(body("To let your agent ask before it invents:"));
3809
+ console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
3810
+ console.log("");
3811
+ return;
3812
+ }
3813
+ await mkdir(dirname(arquivo), { recursive: true });
3814
+ /**
3815
+ * A PASTA SE DECLARA GERENCIADA ANTES DE RECEBER O ARQUIVO: sem isto, o HTML apareceria no
3816
+ * `git status` de quem acabou de pedir um relatório, e não um arquivo versionado.
3817
+ */
3818
+ await writeGovernanceIgnore(root).catch(() => null);
3819
+ await writeFile(arquivo, html(), "utf8");
3820
+ console.log("");
3821
+ console.log(body(` ${paint.strong(relative(root, arquivo))}`));
3822
+ console.log(body(` ${paint.dim(`file://${arquivo}`)}`));
3823
+ console.log("");
3824
+ console.log(body("To let your agent ask before it invents:"));
3825
+ console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
3826
+ console.log("");
3827
+ }
3828
+ /**
3829
+ * A PERGUNTA DE UMA TECLA, e o silêncio de quem não tem terminal.
3830
+ *
3831
+ * Fora de um terminal a resposta é NÃO, sempre: ninguém está lá para responder, e assumir o sim
3832
+ * escreveria no repositório de um CI que só queria ler. Enter é sim porque a pessoa acabou de ler
3833
+ * a oferta na linha acima - exigir uma letra para receber o que a tela ofereceu é atrito sem
3834
+ * proteção nenhuma.
3835
+ */
3836
+ async function quer(pergunta) {
3837
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
3838
+ return false;
3839
+ const { createInterface } = await import("node:readline/promises");
3840
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3841
+ try {
3842
+ console.log("");
3843
+ const resposta = (await rl.question(pergunta)).trim().toLowerCase();
3844
+ return resposta === "" || resposta === "y" || resposta === "yes";
3845
+ }
3846
+ catch {
3847
+ return false;
3848
+ }
3849
+ finally {
3850
+ rl.close();
3851
+ }
3852
+ }
3853
+ /**
3854
+ * AS SEÇÕES QUE CONTINUAM NA TELA, e o critério é um só: elas mudam o comando seguinte.
3855
+ *
3856
+ * Um root com três apps mede a média de três paletas, o que é diagnóstico e não sistema - e a saída
3857
+ * já traz o `--scope`/`--usage` exato. Um `var()` que não resolve não pinta cor nenhuma no
3858
+ * navegador dele. As outras quinze seções descrevem; estas duas pedem uma decisão.
3859
+ */
3860
+ const DECIDEM = [
3861
+ "This root holds several projects",
3862
+ "These resolve to nothing",
3863
+ ];
3723
3864
  function sayReach(c) {
3724
3865
  const reach = ladderReach(c.declared);
3725
3866
  console.log("");
@@ -4332,6 +4473,16 @@ export async function runImport(opts) {
4332
4473
  if (!opts.inspect)
4333
4474
  phase(1, 3, "Measuring the repository");
4334
4475
  console.log(section("Reading your project"));
4476
+ /**
4477
+ * A PARTIR DAQUI O `inspect` GUARDA EM VEZ DE IMPRIMIR - ver `transcript.ts`.
4478
+ *
4479
+ * As dezessete seções continuam sendo escritas exatamente como sempre foram; o que muda é o
4480
+ * destino. Elas vão para o relatório, e o terminal fica com os quatro blocos que uma pessoa
4481
+ * usa para DECIDIR. Medido na saída do `codelevel-monorepo` em 18/09: 250 linhas, das quais 94
4482
+ * eram a mesma frase sobre nome de parte - 40% do relatório num detalhe sem ação possível.
4483
+ */
4484
+ if (opts.inspect)
4485
+ ligarCaptura();
4335
4486
  /**
4336
4487
  * UMA MEDIÇÃO POR ESCOPO, E DEPOIS A FUSÃO.
4337
4488
  *
@@ -4448,13 +4599,8 @@ export async function runImport(opts) {
4448
4599
  */
4449
4600
  if (opts.inspect) {
4450
4601
  sayReach(census);
4451
- console.log("");
4452
- console.log(section("Your agent cannot ask any of this yet"));
4453
- console.log(body("Nothing was written, and nothing was sent."));
4454
- console.log("");
4455
- console.log(body("To let it ask before it invents:"));
4456
- console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
4457
- console.log("");
4602
+ const transcrito = desligarCaptura();
4603
+ await mostrarInspecao(census, root, transcrito);
4458
4604
  return;
4459
4605
  }
4460
4606
  const out = opts.census ?? join(root, "_synthesisui", "census.json");
@@ -0,0 +1,205 @@
1
+ import { semCor } from "./transcript.js";
2
+ /**
3
+ * O QUE O TERMINAL DIRIA, LIDO DE VOLTA COMO DOCUMENTO.
4
+ *
5
+ * A régua `── Título ──────` é a única marca estrutural que a saída tem, e ela é desenhada num
6
+ * lugar só (`section`), então é ela que separa os capítulos. Tudo antes da primeira régua é ruído
7
+ * de progresso e não entra: o documento começa no primeiro capítulo, não no meio do carregamento.
8
+ */
9
+ export function secoesDe(linhas) {
10
+ const secoes = [];
11
+ for (const bruta of linhas) {
12
+ for (const linha of semCor(bruta).split("\n")) {
13
+ const cabeca = /^──\s(.+?)\s─*$/.exec(linha.trimEnd());
14
+ if (cabeca) {
15
+ secoes.push({ titulo: cabeca[1].trim(), linhas: [] });
16
+ continue;
17
+ }
18
+ if (secoes.length === 0)
19
+ continue;
20
+ secoes[secoes.length - 1].linhas.push(linha.replace(/\s+$/, ""));
21
+ }
22
+ }
23
+ /** Capítulo que não disse nada não vira capítulo - uma régua sozinha não é conteúdo. */
24
+ return secoes
25
+ .map((s) => ({ ...s, linhas: aparar(s.linhas) }))
26
+ .filter((s) => s.linhas.length > 0);
27
+ }
28
+ /** Tira as linhas em branco das pontas, e nunca as do meio - lá elas são respiro. */
29
+ function aparar(linhas) {
30
+ let i = 0;
31
+ let f = linhas.length;
32
+ while (i < f && linhas[i].trim() === "")
33
+ i += 1;
34
+ while (f > i && linhas[f - 1].trim() === "")
35
+ f -= 1;
36
+ return linhas.slice(i, f);
37
+ }
38
+ function escapar(s) {
39
+ return s
40
+ .replace(/&/g, "&amp;")
41
+ .replace(/</g, "&lt;")
42
+ .replace(/>/g, "&gt;")
43
+ .replace(/"/g, "&quot;");
44
+ }
45
+ export function relatorioHtml(input) {
46
+ const { cabecalho, destaques, secoes } = input;
47
+ const quando = legivel(cabecalho.medidoEm);
48
+ const indice = secoes
49
+ .map((s, i) => `<li><a href="#s${i}">${escapar(s.titulo)}</a></li>`)
50
+ .join("\n ");
51
+ const corpo = secoes
52
+ .map((s, i) => ` <section id="s${i}">
53
+ <h2>${escapar(s.titulo)}</h2>
54
+ <pre>${escapar(s.linhas.join("\n"))}</pre>
55
+ </section>`)
56
+ .join("\n");
57
+ const numeros = destaques
58
+ .map((d) => ` <div class="stat">
59
+ <strong>${escapar(d.numero)}</strong>
60
+ <span>${escapar(d.rotulo)}</span>
61
+ </div>`)
62
+ .join("\n");
63
+ return `<!doctype html>
64
+ <html lang="en">
65
+ <head>
66
+ <meta charset="utf-8">
67
+ <meta name="viewport" content="width=device-width, initial-scale=1">
68
+ <title>${escapar(cabecalho.repo)} - read by SynthesisUI</title>
69
+ <style>
70
+ ${estilo()}
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <header>
75
+ <p class="kicker">Read by SynthesisUI</p>
76
+ <h1>${escapar(cabecalho.repo)}</h1>
77
+ <p class="meta">Read ${escapar(quando)} by CLI ${escapar(cabecalho.cli)} &middot; nothing left this machine</p>
78
+ </header>
79
+
80
+ <div class="wrap">
81
+ <div class="stats">
82
+ ${numeros}
83
+ </div>
84
+
85
+ <nav>
86
+ <p class="kicker">What is in here</p>
87
+ <ol>
88
+ ${indice}
89
+ </ol>
90
+ </nav>
91
+
92
+ <main>
93
+ ${corpo}
94
+ </main>
95
+
96
+ <footer>
97
+ <p>This file lives in your project and was never sent anywhere. Re-run the command to refresh it.</p>
98
+ </footer>
99
+ </div>
100
+ </body>
101
+ </html>
102
+ `;
103
+ }
104
+ /** A data em palavras. Data ilegível some da frase em vez de imprimir `Invalid Date`. */
105
+ function legivel(iso) {
106
+ if (!iso || Number.isNaN(Date.parse(iso)))
107
+ return "just now";
108
+ return new Date(iso).toLocaleDateString("en-US", {
109
+ month: "short",
110
+ day: "numeric",
111
+ year: "numeric",
112
+ });
113
+ }
114
+ /**
115
+ * A FOLHA, E ELA É INTEIRA DAQUI: nenhuma família remota, nenhum reset de terceiro. As famílias são
116
+ * as genéricas do sistema de quem abrir, porque baixar uma fonte para desenhar um relatório sobre o
117
+ * repositório de alguém contradiz o comando que não manda nada para lugar nenhum.
118
+ *
119
+ * E ela imprime: `@media print` existe porque o navegador já exporta PDF, e um relatório que sai
120
+ * torto no papel obriga a construir um gerador de PDF para resolver o que uma regra resolve.
121
+ */
122
+ function estilo() {
123
+ return `:root {
124
+ color-scheme: light dark;
125
+ --ink: #16161a;
126
+ --ink-soft: #55555f;
127
+ --line: #e4e4e9;
128
+ --paper: #ffffff;
129
+ --canvas: #f6f6f8;
130
+ --accent: #b4530a;
131
+ }
132
+ @media (prefers-color-scheme: dark) {
133
+ :root {
134
+ --ink: #edecea;
135
+ --ink-soft: #a0a0aa;
136
+ --line: #2a2a31;
137
+ --paper: #17171b;
138
+ --canvas: #0f0f12;
139
+ --accent: #e9a13b;
140
+ }
141
+ }
142
+ * { box-sizing: border-box; }
143
+ body {
144
+ margin: 0;
145
+ background: var(--canvas);
146
+ color: var(--ink);
147
+ font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
148
+ }
149
+ header {
150
+ padding: 56px 24px 40px;
151
+ border-bottom: 1px solid var(--line);
152
+ background: var(--paper);
153
+ }
154
+ .wrap, header > * { max-width: 900px; margin-inline: auto; }
155
+ .wrap { padding: 0 24px 72px; }
156
+ .kicker {
157
+ margin: 0;
158
+ font: 600 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
159
+ letter-spacing: 0.16em;
160
+ text-transform: uppercase;
161
+ color: var(--accent);
162
+ }
163
+ h1 { margin: 12px 0 8px; font-size: 34px; line-height: 1.15; letter-spacing: -0.02em; }
164
+ .meta { margin: 0; color: var(--ink-soft); font-size: 14px; }
165
+ .stats {
166
+ display: grid;
167
+ gap: 12px;
168
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
169
+ margin: -28px 0 40px;
170
+ }
171
+ .stat {
172
+ background: var(--paper);
173
+ border: 1px solid var(--line);
174
+ border-radius: 14px;
175
+ padding: 18px 20px;
176
+ }
177
+ .stat strong { display: block; font-size: 26px; letter-spacing: -0.02em; }
178
+ .stat span { display: block; margin-top: 4px; color: var(--ink-soft); font-size: 13px; }
179
+ nav { margin-bottom: 40px; }
180
+ nav ol { margin: 10px 0 0; padding-left: 20px; color: var(--ink-soft); }
181
+ nav li { margin: 3px 0; }
182
+ nav a { color: inherit; }
183
+ nav a:hover { color: var(--accent); }
184
+ section {
185
+ background: var(--paper);
186
+ border: 1px solid var(--line);
187
+ border-radius: 14px;
188
+ padding: 22px 24px;
189
+ margin-bottom: 16px;
190
+ }
191
+ h2 { margin: 0 0 12px; font-size: 17px; letter-spacing: -0.01em; }
192
+ pre {
193
+ margin: 0;
194
+ white-space: pre-wrap;
195
+ word-break: break-word;
196
+ font: 12.5px/1.65 ui-monospace, SFMono-Regular, Menlo, monospace;
197
+ color: var(--ink-soft);
198
+ }
199
+ footer { margin-top: 40px; color: var(--ink-soft); font-size: 13px; }
200
+ @media print {
201
+ body { background: #fff; }
202
+ .stat, section { break-inside: avoid; border-color: #ddd; }
203
+ nav { display: none; }
204
+ }`;
205
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * O QUE O TERMINAL TERIA IMPRIMIDO, GUARDADO EM VEZ DE MOSTRADO.
3
+ *
4
+ * O QUE O CLIENTE GANHA: a saída do `inspect` volta a caber numa tela, e nada do que ela dizia se
5
+ * perde - o texto inteiro vai para o relatório que ele abre quando quiser.
6
+ *
7
+ * POR QUE AQUI E NÃO EM CADA IMPRESSOR: são dezessete seções escritas em dezessete lugares, e a
8
+ * leitura delas é a mesma coisa que a esteira já sabe dizer. Reescrevê-las num gerador de HTML
9
+ * criaria duas redações da mesma medição - e no dia em que uma fosse editada, o relatório passaria
10
+ * a afirmar algo que o terminal não afirma mais. Capturando, existe UMA fonte: o que a esteira diz.
11
+ *
12
+ * ELE É DELIBERADAMENTE BURRO: liga, guarda, desliga. Sem filtro, sem formatação e sem saber o que
13
+ * é seção - quem entende do assunto é quem transforma isto em documento.
14
+ */
15
+ let guardadas = null;
16
+ let originalLog = null;
17
+ /**
18
+ * A REDE DE SEGURANÇA, e ela existe por um motivo específico: uma medição que lança no meio
19
+ * deixaria o `console.log` desviado pelo resto do processo, e a pessoa veria o comando morrer sem
20
+ * uma linha de explicação. O terminal volta ao normal mesmo quando ninguém se lembra de desligar.
21
+ */
22
+ process.once("exit", () => {
23
+ if (originalLog)
24
+ console.log = originalLog;
25
+ });
26
+ /** Começa a guardar em vez de imprimir. Chamar duas vezes não empilha - a segunda é um no-op. */
27
+ export function ligarCaptura() {
28
+ if (guardadas)
29
+ return;
30
+ guardadas = [];
31
+ originalLog = console.log;
32
+ console.log = (...args) => {
33
+ guardadas?.push(args.map((a) => (typeof a === "string" ? a : String(a))).join(" "));
34
+ };
35
+ }
36
+ /** Devolve o terminal a quem é dele, e entrega o que foi guardado. */
37
+ export function desligarCaptura() {
38
+ const linhas = guardadas ?? [];
39
+ if (originalLog)
40
+ console.log = originalLog;
41
+ guardadas = null;
42
+ originalLog = null;
43
+ return linhas;
44
+ }
45
+ /** O caractere de escape ANSI, montado por código para não viajar cru no arquivo. */
46
+ const ESC = String.fromCharCode(27);
47
+ /** Tira a cor: o que vai para um arquivo não carrega sequência de escape de terminal. */
48
+ export function semCor(line) {
49
+ return line.split(new RegExp(`${ESC}\\[[0-9;]*m`)).join("");
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.429",
3
+ "version": "0.16.430",
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": {