synthesisui 0.16.149 → 0.16.152

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.
@@ -71,7 +71,7 @@ async function wireHook(root, command) {
71
71
  * O MESMO SERVIDOR MCP, NOS CAMINHOS QUE CADA AGENTE LÊ.
72
72
  *
73
73
  * `.mcp.json` é o escopo-de-projeto do Claude Code; o Cursor lê `.cursor/mcp.json` com o MESMO shape
74
- * (`mcpServers`). Então cobrir os dois é o mesmo JSON num segundo caminho, e as 11 ferramentas passam
74
+ * (`mcpServers`). Então cobrir os dois é o mesmo JSON num segundo caminho, e as ferramentas passam
75
75
  * a existir nos dois editores sem uma linha de motor novo - era a lacuna mais barata da análise de DX
76
76
  * (05/08).
77
77
  *
@@ -94,9 +94,23 @@ async function wireMcpAt(root, version, rel) {
94
94
  synthesisui: {
95
95
  type: "stdio",
96
96
  command: "npx",
97
+ /**
98
+ * O MCP FLUTUA, E O HOOK NÃO - e a diferença é o custo, não o gosto.
99
+ *
100
+ * O comentário do `hookCommand` mede 720ms-1,5s POR ESCRITA e conclui que um hook cujo
101
+ * comportamento muda sob você a cada edit é indebugável. Está certo, e não vale para aqui:
102
+ * um servidor MCP sobe UMA VEZ quando o editor abre a sessão e fica estável durante ela.
103
+ *
104
+ * Pinado, ele nunca ficava mais novo - e a regra que manda o agente comparar `ledger.cli`
105
+ * com a versão do servidor quase nunca disparava, porque os dois congelavam juntos no dia
106
+ * do `connect` (dono, 06/08). Com `@latest`, um leitor publicado hoje alcança o repo dele
107
+ * na próxima vez que ele abrir o editor, sem ninguém rodar nada.
108
+ *
109
+ * O install local continua ganhando: quem tem o CLI no `node_modules` escolheu a versão.
110
+ */
97
111
  args: local
98
112
  ? ["--no-install", "synthesisui", "mcp"]
99
- : [`synthesisui@${version}`, "mcp"],
113
+ : ["synthesisui@latest", "mcp"],
100
114
  },
101
115
  },
102
116
  }, null, 2)}\n`, "utf8");
package/dist/claude-md.js CHANGED
@@ -166,6 +166,38 @@ shadcn components wear this system and need no edits from you. Without it they
166
166
  stay on shadcn's defaults, which looks like the design system failing rather than
167
167
  one import missing - so check for it before you add a shadcn component, and add
168
168
  it if it is not there.`;
169
+ /**
170
+ * A REGRA QUE TIRA O "O DEV FAZ ALGO", e ela vale para todo agente que lê este bloco.
171
+ *
172
+ * Um leitor novo do CLI precisa olhar o código do cliente, e a esteira não guarda esse código - então
173
+ * a medição é irredutível na máquina dele. O que era redutível é o GATILHO: até 06/08 era uma pessoa
174
+ * lembrando de rodar `sync` num momento em que ela precisava escolher, e um conserto que depende disso
175
+ * alcança a minoria dos clientes.
176
+ *
177
+ * A checagem é LOCAL e grátis: o censo grava qual CLI o mediu (`ledger.cli`) e o servidor MCP É o CLI,
178
+ * então comparar as duas versões é aritmética em disco. Nada aqui telefona para casa - é a mesma
179
+ * decisão que mantém o hook instalado.
180
+ */
181
+ const REFRESH = `
182
+
183
+ **If the measurement is older than the reader, refresh it before you write UI.**
184
+ Read \`_synthesisui/census.json\` → \`ledger.cli\` and compare it with the version
185
+ of the \`synthesisui\` MCP server you are talking to. When they differ, call
186
+ \`refresh_system\` once. It re-measures this repo with the current reader, reuses
187
+ the reading that was already authored, and sends it - a newer reader sees styles
188
+ the old one could not, and until it runs, this project's system is missing them.
189
+
190
+ It never asks anyone anything, and it never overwrites a recipe a person wrote on
191
+ the screen: when a send would rewrite one, it refuses and names them. If it says
192
+ nothing changed, nothing changed - carry on.
193
+
194
+ **And if it reports that a newer CLI is published, say so to the person and stop
195
+ there.** You cannot fix that one: the check that runs after your writes is pinned
196
+ to the version installed at setup, and the tools you are holding were started by
197
+ the editor. One command rewires both - \`npx synthesisui@latest connect\` - and it
198
+ takes effect the next time the editor starts a session. Telling them is the whole
199
+ job; doing it yourself would leave them with a file that says one version and a
200
+ process running another.`;
169
201
  const VERIFY = `
170
202
 
171
203
  **Verify with a build, not a running server.** If you do start a dev server, stop
@@ -185,7 +217,7 @@ const SELF_CHECK_MANUAL = `
185
217
  It names the token for every hardcoded value it finds. Fix what it names, run it
186
218
  again, and only then move to the next thing. Do not batch this up for the end -
187
219
  a component checked while you still have its context is a two-line fix; the same
188
- component found in a repo-wide audit next week is an archaeology exercise.${REFUSAL}
220
+ component found in a repo-wide audit next week is an archaeology exercise.${REFUSAL}${REFRESH}
189
221
 
190
222
  Better than remembering: \`npx synthesisui@latest connect\` installs the check as
191
223
  an editor hook, so it runs on its own and this paragraph stops being your job.${VERIFY}`;
@@ -196,7 +228,7 @@ token this project already has for anything hardcoded. You do not need to run
196
228
  \`doctor\` by hand, and you should not wait for a final pass.
197
229
 
198
230
  When it names something, fix it in that file before moving to the next one -
199
- you still have the context, so it is a two-line change.${REFUSAL}${VERIFY}`;
231
+ you still have the context, so it is a two-line change.${REFUSAL}${REFRESH}${VERIFY}`;
200
232
  /**
201
233
  * THE INTERFACE LANGUAGE, READ FROM THE PROJECT RATHER THAN ASKED FOR.
202
234
  *
@@ -6,6 +6,7 @@ import { body, paint, section, snippet } from "../output.js";
6
6
  import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "../skill-import.js";
7
7
  import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
8
8
  import { ci } from "./ci.js";
9
+ import { MCP_TOOL_COUNT } from "./mcp.js";
9
10
  /**
10
11
  * `synthesisui connect` - put the three layers where they actually run.
11
12
  *
@@ -44,13 +45,16 @@ export async function connect(opts) {
44
45
  console.log(snippet([wired.command]));
45
46
  }
46
47
  /**
47
- * OS CAMINHOS DE MCP, POR NOME - e o número certo. Dizia "four tools" desde quando eram quatro; são
48
- * 11, e um número velho na primeira tela é a coisa mais fácil de perder a confiança de alguém.
48
+ * OS CAMINHOS DE MCP, POR NOME - e o número vem da LISTA, não da minha memória.
49
+ *
50
+ * Dizia "four tools" desde quando eram quatro; eu troquei por 11 contando o `serverInfo` como
51
+ * ferramenta, e eram 10. Um número que alguém digita envelhece na primeira ferramenta nova - este é
52
+ * lido de `MCP_TOOL_COUNT`, que é `TOOLS.length`.
49
53
  */
50
54
  if (want.mcp && wired.mcp !== "skipped") {
51
55
  for (const one of wired.mcp)
52
56
  console.log(body(one.status === "added"
53
- ? `✓ ${one.path.padEnd(22)} 11 tools, so the agent can ask instead of guess`
57
+ ? `✓ ${one.path.padEnd(22)} ${MCP_TOOL_COUNT} tools, so the agent can ask instead of guess`
54
58
  : `· ${one.path.padEnd(22)} already had it`));
55
59
  }
56
60
  /**
@@ -672,10 +672,20 @@ export async function doctor(opts) {
672
672
  * today the honest answer was that nobody knew.
673
673
  */
674
674
  if (hasSystem && measurable && scopes.length === 0) {
675
+ /**
676
+ * E A CONTAGEM DE COINCIDÊNCIAS VAI JUNTO - ver `crossFamily` em `scan.ts`.
677
+ *
678
+ * É a única classe de bug NOSSO que a plataforma não tinha como ver: ela nasce no casamento entre
679
+ * um valor dele e o nome de um token nosso, então só a máquina dele mede. Zero também é resposta -
680
+ * a biblioteca dele deu 0 de 76 no mesmo dia em que o app deu 14 de 129.
681
+ */
682
+ const crossFamily = d.findings.filter((f) => f.crossFamily).length;
675
683
  await appendEvent(root, {
676
684
  kind: "doctor",
677
685
  at: new Date().toISOString(),
678
686
  coverage: d.coverage,
687
+ named: d.named,
688
+ ...(crossFamily > 0 ? { crossFamily } : {}),
679
689
  });
680
690
  }
681
691
  if (hasSystem && scopes.length === 0) {
@@ -7,6 +7,7 @@ import { nearestToken, tokenFor } from "../doctor/tokens.js";
7
7
  import { repoStateOf } from "../repo-state.js";
8
8
  import { component } from "./component.js";
9
9
  import { loadSystem, walkAll } from "./doctor.js";
10
+ import { installedSlug, remeasure } from "./sync.js";
10
11
  const send = (msg) => {
11
12
  process.stdout.write(`${JSON.stringify(msg)}\n`);
12
13
  };
@@ -37,6 +38,11 @@ async function capturing(run) {
37
38
  }
38
39
  // ── The tools ────────────────────────────────────────────────────────────────
39
40
  const TOOLS = [
41
+ {
42
+ name: "refresh_system",
43
+ description: "Re-measure this repository with the reader you are running and send it to the design system, reusing the reading already authored. Call this when `_synthesisui/census.json` was measured by an OLDER CLI than this server - the tool tells you the two versions and refuses politely when there is nothing to refresh. It never asks a person anything: when a recipe in the draft was written on the screen and this send would rewrite it, it refuses and names them instead. Nothing on the platform is deleted and no published version moves.",
44
+ inputSchema: { type: "object", properties: {} },
45
+ },
40
46
  {
41
47
  name: "check_file",
42
48
  description: "Check one file (or folder) against the installed design system. Returns token coverage, every hardcoded value with the token this project already has for it, and any --ds- name the system does not declare. Run this after writing or editing any UI file, before moving on.",
@@ -187,6 +193,14 @@ const TOOLS = [
187
193
  },
188
194
  },
189
195
  ];
196
+ /**
197
+ * QUANTAS FERRAMENTAS ESTE SERVIDOR SERVE, lido da lista.
198
+ *
199
+ * O `connect` imprimia o número à mão: ficou em "four tools" muito depois de serem mais, e quando eu
200
+ * corrigi escrevi 11 contando o `serverInfo` como ferramenta - eram 10. Um número digitado envelhece na
201
+ * primeira ferramenta nova.
202
+ */
203
+ export const MCP_TOOL_COUNT = TOOLS.length;
190
204
  async function checkFile(root, path) {
191
205
  const { table } = await loadSystem(root);
192
206
  if (table.byName.size === 0)
@@ -855,6 +869,38 @@ cli) {
855
869
  });
856
870
  return text(`Filed as ${r.id}. It shows up in \`synthesisui doctor\` until a person authors it or closes it - keep building with your workaround, and say in your summary that the request exists.`);
857
871
  }
872
+ case "refresh_system": {
873
+ /**
874
+ * O ELO QUE TIRA O "O DEV FAZ ALGO" - e ele é uma ferramenta e não um comando de propósito.
875
+ *
876
+ * Um leitor novo do CLI precisa OLHAR o código do cliente, e a gente não guarda esse código: a
877
+ * medição é irredutível na máquina dele. O que era redutível é o GATILHO - até 06/08 era uma
878
+ * pessoa lembrando de um comando num momento que ela precisava escolher. Aqui é o agente,
879
+ * dentro de uma sessão que ela já começou, seguindo a regra que o bloco gerenciado carrega.
880
+ *
881
+ * Chama o MESMO `remeasure` do `sync`, capturando a saída: um segundo caminho de medição
882
+ * discordaria do primeiro na primeira mudança, e a diferença apareceria no sistema de um cliente.
883
+ *
884
+ * ENVIA SOZINHO quando não há nada em risco, e RECUSA nomeando quando há - sem TTY o guarda de
885
+ * rascunho responde "não" por construção, então o pior caso automatizado é uma resposta dizendo
886
+ * o que não foi feito e por quê (dono, 06/08).
887
+ */
888
+ const slug = await installedSlug(root);
889
+ if (!slug)
890
+ return text("No system installed here that belongs to a dashboard row, so there is nothing to refresh. `synthesisui add <slug>` installs one; an adopted system is described here and owned elsewhere.");
891
+ const token = await readToken();
892
+ if (!token)
893
+ return text("Not signed in, so nothing could be sent. A person has to run `synthesisui login` once in the terminal - it opens a browser, and I cannot complete it for them.");
894
+ const { out } = await capturing(() => remeasure({
895
+ root,
896
+ base: resolveRegistry(),
897
+ token,
898
+ slug,
899
+ cli: VERSION,
900
+ }));
901
+ return text(out.trim() ||
902
+ "Nothing was reported by the refresh - which means it found nothing to measure.");
903
+ }
858
904
  case "request_token": {
859
905
  const name = String(args.name ?? "");
860
906
  // TRIAGE AT FILING: if the system's own contract already promises this
@@ -139,6 +139,12 @@ export async function status(opts) {
139
139
  : `${owned.slug} on the platform did not come from an import, so there is no measurement to compare.`));
140
140
  if (remote.hasCensus)
141
141
  console.log(body(paint.dim("`synthesisui sync` measures this repo again with today's reader and sends it.")));
142
+ /** E a distância até o leitor publicado, com o comando que a fecha - ver `sync`. */
143
+ if (remote.publishedCli && opts.cli && remote.publishedCli !== opts.cli) {
144
+ console.log("");
145
+ console.log(body(`Your CLI is ${opts.cli} and ${remote.publishedCli} is published - a newer reader sees styles this one cannot.`));
146
+ console.log(paint.blue(snippet(["npx synthesisui@latest connect"])));
147
+ }
142
148
  }
143
149
  /** "2 hours ago", sem dependência - e `Invalid Date` nunca chega à tela. */
144
150
  function ageOf(iso) {
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { readToken, resolveRegistry } from "../config.js";
4
4
  import { readEvents } from "../doctor/ledger.js";
5
5
  import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
6
- import { body, paint, section } from "../output.js";
6
+ import { body, paint, section, snippet } from "../output.js";
7
7
  import { repoStateOf } from "../repo-state.js";
8
8
  import { resolveReadParts, takeCensus } from "./import.js";
9
9
  /**
@@ -53,32 +53,7 @@ export async function sync(opts) {
53
53
  console.log(body(" synthesisui login"));
54
54
  return;
55
55
  }
56
- // Which system does this project's record belong to? The .lock says.
57
- const dsDir = join(root, "_synthesisui", "ds");
58
- let slug = null;
59
- try {
60
- const slugs = (await readdir(dsDir, { withFileTypes: true }))
61
- .filter((e) => e.isDirectory())
62
- .map((e) => e.name);
63
- for (const s of slugs) {
64
- const raw = await readFile(join(dsDir, s, ".lock"), "utf8").catch(() => "");
65
- try {
66
- const lock = JSON.parse(raw);
67
- // An adopted system is described, not owned on the platform - there is
68
- // no dashboard row to sync into (yet).
69
- if (lock.slug && !lock.adopted) {
70
- slug = lock.slug;
71
- break;
72
- }
73
- }
74
- catch {
75
- // unreadable lock - try the next
76
- }
77
- }
78
- }
79
- catch {
80
- // no _synthesisui at all
81
- }
56
+ const slug = await installedSlug(root);
82
57
  if (!slug) {
83
58
  console.log(section("Sync"));
84
59
  console.log(body("No installed system here, so there is nowhere to sync to."));
@@ -180,7 +155,7 @@ export async function sync(opts) {
180
155
  * avisar.
181
156
  * 5. envia para a rota que atualiza o RASCUNHO pelo slug - nada publicado se move.
182
157
  */
183
- async function remeasure(args) {
158
+ export async function remeasure(args) {
184
159
  const { root, base, token, slug } = args;
185
160
  const head = await fetch(`${base}/api/registry/ds/${slug}/reading`, {
186
161
  headers: { Authorization: `Bearer ${token}` },
@@ -202,6 +177,20 @@ async function remeasure(args) {
202
177
  console.log(body(`"${slug}" did not come from an import, so there is no measurement to refresh. Import it first: npx synthesisui import`));
203
178
  return;
204
179
  }
180
+ /**
181
+ * A DISTÂNCIA ATÉ O CLI PUBLICADO, dita onde ele já está.
182
+ *
183
+ * O hook fica pinado na versão do dia do `connect` - por um motivo medido: 720ms por escrita, e um
184
+ * check que muda sob você é indebugável. O preço é que ele não avança sozinho, e a única pessoa que
185
+ * pode movê-lo é quem roda o comando. Então o número é dito aqui, com o comando ao lado, em vez de
186
+ * viver numa tela que alguém precisa lembrar de abrir.
187
+ */
188
+ if (stored.publishedCli && args.cli && stored.publishedCli !== args.cli) {
189
+ console.log(section("A newer reader is published"));
190
+ console.log(body(`You are running ${args.cli} and ${stored.publishedCli} is published. The measurement below uses ${args.cli}.`));
191
+ console.log(paint.blue(snippet(["npx synthesisui@latest connect"])));
192
+ console.log(body(paint.faint("That rewires the hook and the tools to the new version - the editor picks them up on the next session.")));
193
+ }
205
194
  console.log(section("Measuring your repository again"));
206
195
  console.log(body(paint.faint(`${stored.scope ? `scope ${stored.scope}` : "the whole repo"} · the reading you already authored travels unchanged`)));
207
196
  const census = await takeCensus(stored.scope ? join(root, stored.scope) : root, {
@@ -296,3 +285,34 @@ async function askToOverwrite() {
296
285
  rl.close();
297
286
  }
298
287
  }
288
+ /**
289
+ * O SLUG QUE ESTE PROJETO TEM INSTALADO, ou `null`.
290
+ *
291
+ * Uma função e não duas cópias: o `sync` e a ferramenta `refresh_system` do MCP precisam da mesma
292
+ * busca, e a regra que ela carrega - um DS ADOTADO é descrito aqui e pertencido em outro lugar, então
293
+ * não tem linha no dashboard para onde sincronizar - discordaria entre duas cópias na primeira vez que
294
+ * alguém a mudasse.
295
+ */
296
+ export async function installedSlug(root) {
297
+ const dsDir = join(root, "_synthesisui", "ds");
298
+ try {
299
+ const slugs = (await readdir(dsDir, { withFileTypes: true }))
300
+ .filter((e) => e.isDirectory())
301
+ .map((e) => e.name);
302
+ for (const name of slugs) {
303
+ const raw = await readFile(join(dsDir, name, ".lock"), "utf8").catch(() => "");
304
+ try {
305
+ const lock = JSON.parse(raw);
306
+ if (lock.slug && !lock.adopted)
307
+ return lock.slug;
308
+ }
309
+ catch {
310
+ // unreadable lock - try the next
311
+ }
312
+ }
313
+ }
314
+ catch {
315
+ // no _synthesisui at all
316
+ }
317
+ return null;
318
+ }
@@ -50,6 +50,9 @@ export async function repoStateOf(projectRoot, slug, cli) {
50
50
  ...(typeof doctor?.phantoms === "number"
51
51
  ? { drifts: doctor.phantoms }
52
52
  : {}),
53
+ ...(typeof doctor?.crossFamily === "number"
54
+ ? { crossFamily: doctor.crossFamily }
55
+ : {}),
53
56
  };
54
57
  /** Só o `cli` não é estado: sem nada do repo, não há o que reportar. */
55
58
  return Object.keys(payload).length > 1 ? payload : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.149",
3
+ "version": "0.16.152",
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": {