synthesisui 0.16.156 → 0.16.158

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.
@@ -45,20 +45,76 @@ export async function hookCommand(root, version) {
45
45
  * the owner reading the output against the file, 27/07). Two wrong sentences
46
46
  * from one wrong variable, and both point somebody at the wrong thing to debug.
47
47
  */
48
+ /**
49
+ * A VERSÃO QUE O HOOK ESTÁ RODANDO, lida do arquivo dele - ou `null` quando não há hook pinado.
50
+ *
51
+ * O hook é PINADO de propósito (ver `hookCommand`): um verificador cujo comportamento muda sob você a
52
+ * cada edit é indebugável. O preço é que ele nunca fica mais novo sozinho, e em 06/08 o agente do dono
53
+ * montou essa comparação à mão - leu `.claude/settings.json`, leu `.mcp.json`, e reportou "0.16.153
54
+ * pinado contra 0.16.156 no MCP". Ele acertou, e nada nosso tinha dito isso a ele.
55
+ *
56
+ * Existe para o servidor MCP dizer a frase curta em vez de o agente ter que descobrir: a ferramenta sabe
57
+ * a própria versão e este leitor sabe a do hook, então a defasagem é aritmética.
58
+ *
59
+ * Instalação local devolve `null`: ali o hook segue o `node_modules` e nunca está atrás por si.
60
+ */
61
+ export async function pinnedHookVersion(root) {
62
+ const settings = await readJson(join(root, ".claude", "settings.json")).catch(() => ({}));
63
+ const hooks = (settings.hooks ?? {});
64
+ const entries = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
65
+ for (const entry of entries)
66
+ for (const h of entry.hooks ?? []) {
67
+ const command = h.command ?? "";
68
+ if (/--no-install synthesisui hook/.test(command))
69
+ return null;
70
+ const m = /synthesisui@(\d+\.\d+\.\d+)/.exec(command);
71
+ if (m)
72
+ return m[1];
73
+ }
74
+ return null;
75
+ }
48
76
  async function wireHook(root, command) {
49
77
  const dir = join(root, ".claude");
50
78
  const path = join(dir, "settings.json");
51
79
  const settings = await readJson(path);
52
80
  const hooks = (settings.hooks ?? {});
53
81
  const post = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
54
- // Idempotent on the COMMAND, not on the whole entry: someone may have
55
- // widened the matcher or added a second hook beside ours, and running this
56
- // again must not undo that.
82
+ /**
83
+ * O CASO QUE ESTA FUNÇÃO NÃO COBRIA, e ele tornava o `connect` uma promessa falsa.
84
+ *
85
+ * Ela achava QUALQUER hook contendo "synthesisui" e devolvia "already there" sem olhar a versão -
86
+ * então um hook pinado nunca subia. Medido em 06/08: o dono rodou `npx synthesisui connect` com o
87
+ * 0.16.157 e a saída disse `· .claude/settings.json already had it / npx synthesisui@0.16.153 hook`.
88
+ * O pin era permanente até alguém editar o arquivo à mão, e a mensagem que a gente acabou de escrever
89
+ * - "rode connect, depois reinicie" - mandava rodar um comando que não fazia a coisa.
90
+ *
91
+ * Agora o comando é REESCRITO no lugar quando ele é nosso e difere. O que a idempotência original
92
+ * protegia continua protegido: o matcher fica como estiver, os hooks vizinhos ficam, e uma segunda
93
+ * entrada nunca é criada - duas entradas rodariam o verificador duas vezes por escrita.
94
+ */
57
95
  const mine = post
58
96
  .flatMap((e) => e.hooks ?? [])
59
97
  .find((h) => (h.command ?? "").includes("synthesisui"));
60
- if (mine)
61
- return { status: "already there", command: mine.command ?? command };
98
+ if (mine) {
99
+ const was = mine.command ?? "";
100
+ /**
101
+ * SÓ UM PIN NOSSO É SUBSTITUÍDO, e a fronteira é estreita de propósito.
102
+ *
103
+ * Um comando SEM versão (`npx synthesisui hook`) ou com instalação local é escolha deliberada de
104
+ * alguém: reescrevê-lo seria pinar a decisão da pessoa, e um spec já protegia isso - o caso do
105
+ * matcher alargado à mão. O que sobe é `synthesisui@<semver>` para outro semver, que é o único caso
106
+ * em que "already there" mentia (dono, 06/08: `connect` do 0.16.157 imprimindo
107
+ * `npx synthesisui@0.16.153 hook` como "already had it").
108
+ */
109
+ const pinnedAt = /synthesisui@(\d+\.\d+\.\d+)/.exec(was)?.[1];
110
+ const proposedAt = /synthesisui@(\d+\.\d+\.\d+)/.exec(command)?.[1];
111
+ if (!pinnedAt || !proposedAt || pinnedAt === proposedAt)
112
+ return { status: "already there", command: was || command };
113
+ mine.command = command;
114
+ await mkdir(dir, { recursive: true });
115
+ await writeFile(path, `${JSON.stringify({ ...settings, hooks: { ...hooks, PostToolUse: post } }, null, 2)}\n`, "utf8");
116
+ return { status: "updated", command, was };
117
+ }
62
118
  post.push({
63
119
  matcher: HOOK_MATCHER,
64
120
  hooks: [{ type: "command", command }],
@@ -83,8 +139,20 @@ async function wireMcpAt(root, version, rel) {
83
139
  const path = join(root, rel);
84
140
  const config = await readJson(path);
85
141
  const servers = (config.mcpServers ?? {});
86
- if (servers.synthesisui)
142
+ /**
143
+ * E O MESMO DEFEITO AQUI. Uma entrada nossa que já existe era aceita sem olhar o CONTEÚDO, então quem
144
+ * conectou antes de 06/08 - quando o MCP passou a flutuar em `@latest` - ficou com um servidor pinado
145
+ * para sempre, e nenhum `connect` o soltava. Só a NOSSA entrada é reescrita; o resto do arquivo é
146
+ * dele.
147
+ */
148
+ const current = servers.synthesisui;
149
+ const pinned = current &&
150
+ Array.isArray(current.args) &&
151
+ current.args.some((a) => /^synthesisui@\d/.test(String(a)));
152
+ if (current && !pinned)
87
153
  return "already there";
154
+ /** Reescrita de uma entrada PINADA: é atualização, e a saída tem que dizer isso. */
155
+ const replacing = Boolean(current && pinned);
88
156
  const local = await exists(join(root, "node_modules", ".bin", "synthesisui"));
89
157
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
90
158
  await writeFile(path, `${JSON.stringify({
@@ -114,7 +182,7 @@ async function wireMcpAt(root, version, rel) {
114
182
  },
115
183
  },
116
184
  }, null, 2)}\n`, "utf8");
117
- return "added";
185
+ return replacing ? "updated" : "added";
118
186
  }
119
187
  /**
120
188
  * Os caminhos de MCP que este repo merece: sempre o do Claude Code, e o do Cursor quando `.cursor/`
@@ -137,11 +205,12 @@ export async function wireAgent(root, version, want) {
137
205
  const proposed = await hookCommand(root, version);
138
206
  const hook = want.hook
139
207
  ? await wireHook(root, proposed)
140
- : { status: "skipped", command: proposed };
208
+ : { status: "skipped", command: proposed, was: undefined };
141
209
  return {
142
210
  // What is actually installed - the only thing worth printing.
143
211
  command: hook.command,
144
212
  hook: hook.status,
213
+ ...(hook.was ? { was: hook.was } : {}),
145
214
  mcp: want.mcp ? await wireMcp(root, version) : "skipped",
146
215
  };
147
216
  }
package/dist/claude-md.js CHANGED
@@ -199,13 +199,22 @@ nothing changed, nothing changed - carry on.
199
199
  about a census taken by an older reader is the worst way to be right - every absence
200
200
  in it may have been filled since, and the reader who trusts it has no way to know.
201
201
 
202
- **And if it reports that a newer CLI is published, say so to the person and stop
203
- there.** You cannot fix that one: the check that runs after your writes is pinned
204
- to the version installed at setup, and the tools you are holding were started by
205
- the editor. One command rewires both - \`npx synthesisui@latest connect\` - and it
206
- takes effect the next time the editor starts a session. Telling them is the whole
207
- job; doing it yourself would leave them with a file that says one version and a
208
- process running another.`;
202
+ **And if it says the design system needs updating, do these three things in order:**
203
+ say the one line, ask, and run it.
204
+
205
+ \`\`\`
206
+ Update the design system before continuing.
207
+ npx synthesisui@latest connect
208
+ \`\`\`
209
+
210
+ Then tell them to close and reopen the editor. Not as a footnote - it is the step
211
+ that makes the other two count: the check that runs after your writes is pinned to
212
+ the version installed at setup, and the tools you are holding were started by the
213
+ editor. Both rewire on a new session and not before, so anything you write in this
214
+ one is still checked by the old reader.
215
+
216
+ Do not explain the pinning to them and stop there. A paragraph is what somebody
217
+ reads; a command they approved and you ran is what changes the repo.`;
209
218
  const VERIFY = `
210
219
 
211
220
  **Verify with a build, not a running server.** If you do start a dev server, stop
@@ -41,7 +41,14 @@ export async function connect(opts) {
41
41
  if (want.hook) {
42
42
  console.log(body(wired.hook === "added"
43
43
  ? "✓ .claude/settings.json the check now runs after every write"
44
- : "· .claude/settings.json already had it"));
44
+ : wired.hook === "updated"
45
+ ? /**
46
+ * A LINHA QUE FALTAVA, e a ausência dela fazia o comando mentir: rodando o 0.16.157, a
47
+ * saída dizia "already had it" e imprimia `npx synthesisui@0.16.153 hook` embaixo
48
+ * (dono, 06/08). Quem lê "already had it" fecha o terminal.
49
+ */
50
+ `✓ .claude/settings.json the check moved from ${wired.was?.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? "an older version"} to this one`
51
+ : "· .claude/settings.json already had it"));
45
52
  console.log(snippet([wired.command]));
46
53
  }
47
54
  /**
@@ -55,7 +62,11 @@ export async function connect(opts) {
55
62
  for (const one of wired.mcp)
56
63
  console.log(body(one.status === "added"
57
64
  ? `✓ ${one.path.padEnd(22)} ${MCP_TOOL_COUNT} tools, so the agent can ask instead of guess`
58
- : `· ${one.path.padEnd(22)} already had it`));
65
+ : one.status === "updated"
66
+ ? /** Uma entrada pinada antes de o MCP flutuar ficava pinada para sempre - nenhum
67
+ * `connect` a soltava, e o agente segurava um leitor velho sem saber. */
68
+ `✓ ${one.path.padEnd(22)} unpinned - it now follows the published reader`
69
+ : `· ${one.path.padEnd(22)} already had it`));
59
70
  }
60
71
  /**
61
72
  * ONDE O BLOCO CAIU, DITO POR NOME.
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
+ import { pinnedHookVersion } from "../agent-wiring.js";
3
4
  import { readToken, resolveRegistry } from "../config.js";
4
5
  import { fileRequest } from "../doctor/requests.js";
5
6
  import { diagnose, scanSource } from "../doctor/scan.js";
@@ -898,8 +899,24 @@ cli) {
898
899
  slug,
899
900
  cli: VERSION,
900
901
  }));
901
- return text(out.trim() ||
902
- "Nothing was reported by the refresh - which means it found nothing to measure.");
902
+ /**
903
+ * E A DEFASAGEM DO HOOK, EM TRÊS LINHAS - não num parágrafo.
904
+ *
905
+ * O hook é pinado (ver `hookCommand`) e o servidor MCP flutua, então eles divergem no dia em que
906
+ * uma versão nova é publicada. Em 06/08 o agente do dono montou essa comparação sozinho, leu os
907
+ * dois arquivos e acertou - e nada nosso tinha dito a ele. Antes disso, a regra do bloco pedia um
908
+ * parágrafo de explicação; um parágrafo é o que se lê e não é o que se faz.
909
+ *
910
+ * Vai aqui e não em toda ferramenta porque a regra manda chamar `refresh_system` uma vez por
911
+ * sessão: um lugar que sempre roda vale mais que sete que podem rodar.
912
+ */
913
+ const pinned = await pinnedHookVersion(root);
914
+ const behind = pinned && pinned !== VERSION ? pinned : null;
915
+ const said = out.trim() ||
916
+ "Nothing was reported by the refresh - which means it found nothing to measure.";
917
+ return text(behind
918
+ ? `${said}\n\nUPDATE THE DESIGN SYSTEM BEFORE CONTINUING.\nThe check that runs after your writes is pinned to ${behind} and these tools are ${VERSION}.\n\n npx synthesisui@latest connect\n\nAsk the person, run it for them, and then say this: close and reopen the editor. The hook and the tools only rewire on a new session, so nothing you write before that is checked by the reader you are holding.`
919
+ : said);
903
920
  }
904
921
  case "request_token": {
905
922
  const name = String(args.name ?? "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.156",
3
+ "version": "0.16.158",
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": {