synthesisui 0.16.360 → 0.16.362

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import { hasCodexBlock, pinnedInCodex, withCodexBlock } from "./codex-mcp.js";
3
4
  const HOOK_MATCHER = "Write|Edit|MultiEdit";
4
5
  /**
5
6
  * Exportado porque a REGRA é uma só: a pasta de uma ferramenta é a evidência de que ela é usada, e
@@ -222,8 +223,56 @@ async function wireMcp(root, version) {
222
223
  path: ".cursor/mcp.json",
223
224
  status: await wireMcpAt(root, version, ".cursor/mcp.json"),
224
225
  });
226
+ /**
227
+ * E O CODEX, quando `.codex/` já existe - a mesma regra de evidência que decide o Cursor e a casa
228
+ * do bloco (`bornWhen`): a pasta da ferramenta é a prova de que ela é usada.
229
+ *
230
+ * Formato diferente, escritor diferente: aquele lê e reescreve um objeto JSON; este é dono de uma
231
+ * região delimitada num TOML que a pessoa também edita. Ver `codex-mcp.ts` para o porquê de não
232
+ * haver parser.
233
+ */
234
+ if (await exists(join(root, ".codex")))
235
+ wrote.push({
236
+ path: ".codex/config.toml",
237
+ status: await wireCodexMcp(root, version),
238
+ });
225
239
  return wrote;
226
240
  }
241
+ /**
242
+ * A REGIÃO DO SYNTHESISUI NO `.codex/config.toml` - escrita só quando muda.
243
+ *
244
+ * `already there` quando o arquivo já diz exatamente isto, e é o que faz um `connect` seguido dizer
245
+ * "nada a mudar" em vez de anunciar uma escrita que não aconteceu.
246
+ */
247
+ /**
248
+ * A VERSÃO QUE ESTAVA PINADA NO `.codex/config.toml` ANTES desta rodada - `null` quando não havia.
249
+ *
250
+ * Variável de módulo e não retorno porque o tipo de `wireMcp` é compartilhado com os dois
251
+ * escritores de JSON, e alargá-lo para um campo que só o Codex preenche faria os outros dois
252
+ * carregarem um `undefined` para sempre.
253
+ */
254
+ let codexWas = null;
255
+ /** O que a rodada anterior havia pinado no Codex - para a tela poder dizer de onde veio. */
256
+ export function codexPinBefore() {
257
+ return codexWas;
258
+ }
259
+ async function wireCodexMcp(root, version) {
260
+ const path = join(root, ".codex", "config.toml");
261
+ const current = await readFile(path, "utf8").catch(() => null);
262
+ const had = current != null && hasCodexBlock(current);
263
+ const next = withCodexBlock(current ?? "", version);
264
+ if (current === next)
265
+ return "already there";
266
+ /**
267
+ * A VERSÃO DE ONDE ELE SAIU, para a linha da tela dizer "veio da 0.16.361" em vez de só
268
+ * "atualizado" - a mesma correção que o hook recebeu em 06/08, quando "already had it" escondia
269
+ * um pin velho.
270
+ */
271
+ codexWas = pinnedInCodex(current ?? "");
272
+ await mkdir(dirname(path), { recursive: true }).catch(() => { });
273
+ await writeFile(path, next, "utf8");
274
+ return had ? "updated" : "added";
275
+ }
227
276
  export async function wireAgent(root, version, want) {
228
277
  const proposed = await hookCommand(root, version);
229
278
  const hook = want.hook
@@ -0,0 +1,85 @@
1
+ /**
2
+ * AS FERRAMENTAS DO SISTEMA DENTRO DO CODEX - a metade que faltava para o agente dele saber tanto
3
+ * quanto o do Claude Code.
4
+ *
5
+ * O QUE O CLIENTE GANHA: num repositório com `.codex/`, o agente dele passa a poder PERGUNTAR ao
6
+ * sistema - como um valor se chama, que componentes existem, o que a esteira não leu - em vez de
7
+ * adivinhar. Antes ele recebia as regras (`AGENTS.md`) e nenhuma ferramenta: sabia o que obedecer e
8
+ * não tinha como consultar.
9
+ *
10
+ * O DEFEITO, medido em 03/09 no repositório do dono: `.codex/config.toml` estava lá desde sempre, o
11
+ * `AGENTS.md` passou a nascer daquela pasta, e as 19 ferramentas continuavam existindo só para o
12
+ * Claude Code e o Cursor. A causa é de formato: aqueles dois compartilham o MESMO objeto JSON
13
+ * (`mcpServers`), então cobrir o segundo custou um `writeFile` a mais. O Codex declara em TOML, sob
14
+ * `[mcp_servers.<nome>]`, e nenhuma linha do nosso escritor de JSON serve.
15
+ *
16
+ * TRÊS NÍVEIS, E A GENTE ESCREVE NO DE PROJETO. O Codex lê `/etc/codex/config.toml`,
17
+ * `~/.codex/config.toml` e `.codex/config.toml`, cada um sobrepondo o de cima. O de projeto é o
18
+ * lugar certo para um servidor que vale para UM repositório - e é o único que não é território de
19
+ * fora: escrever no `~/.codex/config.toml` seria a mesma fronteira do `.zshrc`, que este produto
20
+ * atravessa só perguntando.
21
+ *
22
+ * SEM PARSER DE TOML, E ISSO É UMA DECISÃO. Escrever TOML genérico exigiria ler o arquivo inteiro,
23
+ * entender e reserializar - e uma reserialização perde comentário, ordem e formatação de quem
24
+ * escreveu aquilo à mão. O `.codex/config.toml` do dono tem um `[tui]` com a barra de status dele:
25
+ * um arquivo que volta reordenado é um arquivo que ele não reconhece.
26
+ *
27
+ * Então a gente é dona de uma REGIÃO DELIMITADA, exatamente como no `CLAUDE.md` e no hook de
28
+ * shell - o resto do arquivo passa por aqui sem ser tocado, byte por byte.
29
+ */
30
+ const BEGIN = "# synthesisui:start";
31
+ const END = "# synthesisui:end";
32
+ /** Já existe região nossa neste arquivo? */
33
+ export function hasCodexBlock(text) {
34
+ return text.includes(BEGIN);
35
+ }
36
+ /**
37
+ * A versão pinada dentro da nossa região - `null` quando não há região ou quando ela não pina.
38
+ *
39
+ * O `connect` usa isto para dizer de ONDE para onde a fiação andou, que é a diferença entre
40
+ * "atualizei" e uma linha que a pessoa fecha sem ler.
41
+ */
42
+ export function pinnedInCodex(text) {
43
+ const start = text.indexOf(BEGIN);
44
+ if (start === -1)
45
+ return null;
46
+ const end = text.indexOf(END, start);
47
+ const region = text.slice(start, end === -1 ? undefined : end);
48
+ return region.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? null;
49
+ }
50
+ /**
51
+ * A REGIÃO, montada. `args` em duas partes porque é assim que o Codex quer a linha de comando:
52
+ * o executável em `command`, e o resto quebrado em array.
53
+ */
54
+ export function codexBlock(version) {
55
+ return [
56
+ BEGIN,
57
+ "# Managed by synthesisui. Edit outside this block; this region is rewritten on connect.",
58
+ "[mcp_servers.synthesisui]",
59
+ 'command = "npx"',
60
+ `args = ["synthesisui@${version}", "mcp"]`,
61
+ END,
62
+ ].join("\n");
63
+ }
64
+ /**
65
+ * O arquivo com a nossa região em dia, e TODO o resto intocado.
66
+ *
67
+ * Sem região: acrescenta no fim, com uma linha em branco antes - um `[mcp_servers.x]` colado no
68
+ * fim de outra tabela TOML entraria DENTRO dela.
69
+ *
70
+ * Com região: troca só o que está entre as marcas.
71
+ *
72
+ * MARCA DE ABERTURA SEM A DE FECHAMENTO significa que alguém editou à mão e o fechamento se
73
+ * perdeu. Aí a gente devolve o arquivo COMO ESTÁ: apagar até o fim seria destruir o que vem
74
+ * depois, e é a mesma decisão que o `withHook` toma no perfil de shell.
75
+ */
76
+ export function withCodexBlock(text, version) {
77
+ const block = codexBlock(version);
78
+ if (!hasCodexBlock(text))
79
+ return `${text.replace(/\n*$/, "")}\n\n${block}\n`.replace(/^\n+/, "");
80
+ const start = text.indexOf(BEGIN);
81
+ const end = text.indexOf(END, start);
82
+ if (end === -1)
83
+ return text;
84
+ return text.slice(0, start) + block + text.slice(end + END.length);
85
+ }
@@ -9,6 +9,7 @@ import { unsentEvents } from "../doctor/ledger.js";
9
9
  import { readRequests } from "../doctor/requests.js";
10
10
  import { declaredReference } from "../group-role.js";
11
11
  import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
12
+ import { fmt, say } from "../lang.js";
12
13
  import { measuredScope } from "../measured-scope.js";
13
14
  import { body, bodyWrapped, paint, section, snippet } from "../output.js";
14
15
  import { SKILLS } from "../skills.js";
@@ -648,7 +649,7 @@ export async function nextStepFor(root) {
648
649
  * traz as linhas em branco de graça, que é o "respiro" pedido.
649
650
  */
650
651
  export function renderNextStep(next, freshSession) {
651
- const lines = [section("Do this next")];
652
+ const lines = [section(say("Do this next"))];
652
653
  /**
653
654
  * NUMERA SÓ QUANDO SÃO DOIS PASSOS. Abrir o agente e falar com ele são duas coisas, e sem o "1" e
654
655
  * o "2" a pessoa lê dois blocos azuis e não sabe se escolhe um ou faz os dois. Com um passo só, o
@@ -656,32 +657,48 @@ export function renderNextStep(next, freshSession) {
656
657
  */
657
658
  const numbered = Boolean(next.open?.length && next.say);
658
659
  if (next.open?.length) {
659
- lines.push(body(numbered ? `1 ${next.headline}` : next.headline), "");
660
+ lines.push(body(numbered ? `1 ${say(next.headline)}` : say(next.headline)), "");
660
661
  lines.push(paint.blue(snippet(next.open)), "");
661
662
  }
662
663
  if (next.say) {
663
664
  if (next.open?.length)
664
- lines.push(body(numbered ? "2 and say" : "and say"), "");
665
+ lines.push(body(numbered ? `2 ${say("and say")}` : say("and say")), "");
665
666
  else
666
- lines.push(body(`${next.headline} and say:`), "");
667
+ lines.push(body(`${say(next.headline)} ${say("and say")}:`), "");
667
668
  lines.push(paint.blue(snippet([next.say])), "");
668
669
  }
669
670
  if (next.run?.length) {
670
- lines.push(body(next.headline), "");
671
+ lines.push(body(say(next.headline)), "");
671
672
  lines.push(paint.blue(snippet(next.run)), "");
672
673
  }
673
- lines.push(...bodyWrapped(next.why).map(paint.dim));
674
+ lines.push(...bodyWrapped(say(next.why)).map(paint.dim));
674
675
  /** O que a flag dispensa, dito - ver `OPENS` em `claude-md.ts`. E concordando: um repo com um
675
676
  * agente só recebe uma linha no singular, porque "those flags" sobre um comando lê como se a
676
677
  * pessoa tivesse perdido uma opção da tela. */
677
678
  if (next.open?.length)
678
679
  lines.push(...bodyWrapped(next.open.length === 1
679
- ? "That flag lets it write without asking each time. Drop it to approve every change yourself."
680
- : "Those flags let it write without asking each time. Drop them to approve every change yourself.").map(paint.dim));
681
- if (freshSession)
682
- lines.push(...bodyWrapped(freshSession.mcp
683
- ? "A new session is what loads the skills and tools just installed, and the project's tools ask for approval once - say yes."
684
- : "A new session is what loads the skills just installed.").map(paint.dim));
680
+ ? say("Drop the flag to approve each write yourself.")
681
+ : say("Drop the flags to approve each write yourself.")).map(paint.dim));
682
+ if (freshSession) {
683
+ const list = freshSession.loads;
684
+ const said = list.length === 0
685
+ ? "and"
686
+ : list.length === 1
687
+ ? list[0]
688
+ : /** O conector também é língua: "and" em inglês, "e" em português - e a vírgula do
689
+ * penúltimo é a mesma nas duas, então só o último elo entra na tabela. */
690
+ fmt("{head} and {last}", {
691
+ head: list.slice(0, -1).join(", "),
692
+ last: String(list.at(-1)),
693
+ });
694
+ lines.push(...bodyWrapped(list.length === 0
695
+ ? say("A new session reads the wiring this run changed.")
696
+ : fmt(list.length === 1
697
+ ? "This run moved {what}, and a new session is what reads it."
698
+ : "This run moved {what}, and a new session is what reads them.", { what: said })).map(paint.dim));
699
+ if (freshSession.mcp)
700
+ lines.push(...bodyWrapped(say("The project's tools ask for approval once - say yes.")).map(paint.dim));
701
+ }
685
702
  return lines.join("\n");
686
703
  }
687
704
  /**
@@ -1,9 +1,10 @@
1
1
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { wireAgent } from "../agent-wiring.js";
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 { body, paint, section, snippet } from "../output.js";
6
+ import { fmt, say } from "../lang.js";
7
+ import { body, bodyWrapped, paint, section, snippet } from "../output.js";
7
8
  import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
8
9
  import { existingRc, hasHook, pinnedInHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
9
10
  import { SKILLS } from "../skills.js";
@@ -173,8 +174,12 @@ version) {
173
174
  return;
174
175
  }
175
176
  console.log("");
176
- console.log(body("Your editor tells you when this repo drifts, once a session. Your terminal does not, and that is where most people notice something is stale."));
177
- console.log(body(paint.faint(`Adding it writes one block to ${rc}. It stays silent when nothing is wrong, never blocks your prompt, and checks at most once an hour.`)));
177
+ /**
178
+ * A OFERTA EM DUAS LINHAS - eram cinco, e ela ficava entre a lista do que foi instalado e a ação
179
+ * recomendada, empurrando para baixo a única coisa que a pessoa procurava. O que ela precisa dizer
180
+ * é o que ganha, o que a gente escreve e onde: nada disso saiu.
181
+ */
182
+ console.log(bodyWrapped(fmt("Your terminal can warn you when this repo drifts - one block in {rc}, silent unless something is wrong, at most once an hour.", { rc })).join("\n"));
178
183
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
179
184
  console.log(body(paint.blue(" npx synthesisui@latest connect --shell")));
180
185
  return;
@@ -227,7 +232,40 @@ export async function connect(opts) {
227
232
  // The block reads the settings we just wrote, so it must be regenerated
228
233
  // after them, not before.
229
234
  const contract = await syncClaudeMd(root);
230
- console.log(section("Connected"));
235
+ console.log(section(say("Connected")));
236
+ /**
237
+ * A LISTA DA TELA, JUNTADA ANTES DE IMPRIMIR - e é isto que faz a rodada silenciosa caber numa
238
+ * linha.
239
+ *
240
+ * O QUE O CLIENTE GANHA: ele lê o que MUDOU, e o que não mudou vira uma linha só. Medido em 03/09
241
+ * na segunda rodada deste comando: 41 linhas na tela, das quais 10 diziam `already had it` /
242
+ * `already current` - um quarto da tela informando ausência de novidade, exatamente entre a pessoa
243
+ * e a única instrução que ela procurava.
244
+ *
245
+ * A DISTINÇÃO NÃO SE PERDE: cada peça que mudou continua nomeada, com o seu detalhe embaixo. O que
246
+ * colapsa é o silêncio, que não tem detalhe para dar.
247
+ */
248
+ const rows = [];
249
+ const row = (moved, line, under) => {
250
+ rows.push({ moved, line, ...(under ? { under } : {}) });
251
+ };
252
+ const flush = () => {
253
+ for (const r of rows) {
254
+ if (!r.moved)
255
+ continue;
256
+ console.log(body(r.line));
257
+ if (r.under)
258
+ console.log(snippet([r.under]));
259
+ }
260
+ const quiet = rows.filter((r) => !r.moved).length;
261
+ if (quiet === 0)
262
+ return;
263
+ console.log(body(paint.dim(quiet === rows.length
264
+ ? say("· nothing to change - this environment is already current")
265
+ : fmt(quiet === 1
266
+ ? "· {n} other piece already current"
267
+ : "· {n} other pieces already current", { n: quiet }))));
268
+ };
231
269
  /**
232
270
  * E O QUE FALTA, DITO EM VOZ ALTA - porque este comando estava mentindo por omissão.
233
271
  *
@@ -240,45 +278,61 @@ export async function connect(opts) {
240
278
  * `CLAUDE.md`, achou o bloco vazio, e não havia nada dizendo que faltava um comando. Lacuna
241
279
  * silenciosa é o que faz um produto correto parecer quebrado.
242
280
  *
243
- * E SÃO DOIS CAMINHOS, não um. A primeira versão desta mensagem mandava `add <slug>` e só - o que
244
- * desorienta justamente quem acabou de rodar `connect` num projeto que AINDA NÃO TEM sistema, que é
245
- * a primeira corrida e o caso mais comum. Para essa pessoa o passo seguinte é a skill que este mesmo
246
- * comando acabou de instalar, e não um comando de terminal. Quem já tem sistema na plataforma é o
247
- * outro caso, e ele também é dito.
281
+ * E ELE DIZ A LACUNA, NÃO O QUE FAZER - corrigido em 03/09, com o dono lendo a própria tela.
282
+ *
283
+ * Ele carregava os dois caminhos ("ask the agent for `/sui-init`" e "`synthesisui add <slug>`"), e
284
+ * o resultado era UMA TELA COM DUAS INSTRUÇÕES para o mesmo passo: esta linha mandava pedir uma
285
+ * slash command, e a ação recomendada, quinze linhas abaixo, mandava dizer uma frase. Quem lê não
286
+ * tem como saber qual das duas é a certa.
287
+ *
288
+ * E A FRASE É A CERTA, por decisão de produto (dono, 03/09): *"as skills são chamadas de acordo
289
+ * com a necessidade que o Claude vai encontrando, não por comando nosso diretamente"*. Nomear
290
+ * `/sui-init` ensina o cliente a dirigir o agente por comando, que é o oposto de como a esteira
291
+ * foi desenhada - e amarra a instrução ao nome de uma skill que pode ser dividida amanhã.
292
+ *
293
+ * `nextStepFor` JÁ COBRE OS DOIS CAMINHOS, e com a mesma distinção: medido sem sistema instalado
294
+ * leva a `list --mine` + `add <slug>`; nunca medido leva à frase. Então isto para de repeti-los e
295
+ * fica com o que só ele diz - a CONSEQUÊNCIA da lacuna, que é o que fez alguém perder tempo em
296
+ * 20/08 achando o bloco do `CLAUDE.md` vazio sem nada explicando por quê.
248
297
  */
249
298
  const anyInstalled = (await installedSlugs(root).catch(() => [])).length > 0;
250
299
  if (!anyInstalled)
251
- console.log(body("· no design system installed here yet - the agent has no index, and memory has nothing to belong to.\n This repo has none yet: ask the agent for `/sui-init`, or `/sui-import-ds` to turn this project into one.\n You already have one: `synthesisui list` shows them, `synthesisui add <slug>` brings it in."));
300
+ console.log(bodyWrapped(say("· no design system installed here yet - the agent has no index, and memory has nothing to belong to.")).join("\n"));
252
301
  /**
253
302
  * O QUE ESTE CLI REESCREVEU NA PASTA DO SISTEMA - dito primeiro, porque é o que a pessoa não sabia
254
303
  * que estava devendo. Ela rodou `connect` para atualizar a fiação; os arquivos do install estarem
255
304
  * velhos era invisível, e o `upgrade` não alcançava (ver `refreshInstall`).
256
305
  */
257
306
  if (refreshed)
258
- console.log(body(`✓ _synthesisui/ds/${refreshed.slug}/ rewritten by this CLI${refreshed.was
307
+ row(true, `✓ _synthesisui/ds/${refreshed.slug}/ rewritten by this CLI${refreshed.was
259
308
  ? ` - it was written by ${refreshed.was}`
260
- : " - it carried no CLI version, so it predates this"}`));
309
+ : " - it carried no CLI version, so it predates this"}`);
261
310
  if (want.hook) {
262
- console.log(body(wired.hook === "added"
263
- ? "✓ .claude/settings.json the check now runs after every write"
264
- : wired.hook === "updated"
265
- ? /**
266
- * A LINHA QUE FALTAVA, e a ausência dela fazia o comando mentir: rodando o 0.16.157, a
267
- * saída dizia "already had it" e imprimia `npx synthesisui@0.16.153 hook` embaixo
268
- * (dono, 06/08). Quem lê "already had it" fecha o terminal.
269
- */
270
- `✓ .claude/settings.json the check moved from ${wired.was?.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? "an older version"} to this one`
271
- : "· .claude/settings.json already had it"));
272
- console.log(snippet([wired.command]));
311
+ row(wired.hook !== "already there", (() => {
312
+ return wired.hook === "added"
313
+ ? say("✓ .claude/settings.json the check now runs after every write")
314
+ : wired.hook === "updated"
315
+ ? /**
316
+ * A LINHA QUE FALTAVA, e a ausência dela fazia o comando mentir: rodando o 0.16.157, a
317
+ * saída dizia "already had it" e imprimia `npx synthesisui@0.16.153 hook` embaixo
318
+ * (dono, 06/08). Quem lê "already had it" fecha o terminal.
319
+ */
320
+ `✓ .claude/settings.json the check moved from ${wired.was?.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? "an older version"} to this one`
321
+ : say("· .claude/settings.json already had it");
322
+ })(), wired.command);
273
323
  /**
274
324
  * A SEGUNDA COSTURA, dita por nome. As onze ferramentas MCP são PULL e o hook de escrita roda
275
325
  * DEPOIS de uma escrita - nenhum dos dois chega a tempo de dizer "você não está logado nesta
276
326
  * máquina" ou "este sistema não sabe de onde foi medido". O `SessionStart` é o único que chega.
277
327
  */
278
328
  if (wired.session !== "skipped")
279
- console.log(body(wired.session === "already there"
280
- ? "· .claude/settings.json the session check was already there"
281
- : `✓ .claude/settings.json every session now opens with what this environment is missing${wired.session === "updated" ? ", pointed at this version" : ""}`));
329
+ row(wired.session !== "already there", wired.session === "already there"
330
+ ? say("· .claude/settings.json the session check was already there")
331
+ : wired.session === "updated"
332
+ ? /** A distinção `added` x `updated` vale desde 06/08: um estado que não se
333
+ * distingue de "nada a fazer" faz a instrução de atualizar mentir. */
334
+ say("✓ .claude/settings.json the session check moved to this one")
335
+ : say("✓ .claude/settings.json each session opens with what is missing"));
282
336
  }
283
337
  /**
284
338
  * OS CAMINHOS DE MCP, POR NOME - e o número vem da LISTA, não da minha memória.
@@ -289,13 +343,22 @@ export async function connect(opts) {
289
343
  */
290
344
  if (want.mcp && wired.mcp !== "skipped") {
291
345
  for (const one of wired.mcp)
292
- console.log(body(one.status === "added"
293
- ? `✓ ${one.path.padEnd(22)} ${MCP_TOOL_COUNT} tools, so the agent can ask instead of guess`
346
+ row(one.status !== "already there", one.status === "added"
347
+ ? `✓ ${one.path.padEnd(22)} ${fmt("{n} tools the agent can ask", { n: MCP_TOOL_COUNT })}`
294
348
  : one.status === "updated"
295
- ? /** Uma entrada pinada antes de o MCP flutuar ficava pinada para sempre - nenhum
296
- * `connect` a soltava, e o agente segurava um leitor velho sem saber. */
297
- `✓ ${one.path.padEnd(22)} unpinned - it now follows the published reader`
298
- : `· ${one.path.padEnd(22)} already had it`));
349
+ ? /**
350
+ * DUAS HISTÓRIAS DIFERENTES SOB O MESMO `updated`, e dizer a errada é pior que não
351
+ * dizer: no Claude Code e no Cursor a entrada é SOLTA (`@latest`), então o que
352
+ * mudou é ela parar de segurar um leitor velho. No Codex a entrada é PINADA numa
353
+ * versão, e o que mudou é de qual para qual - a mesma correção que o hook recebeu
354
+ * em 06/08, quando "already had it" escondia um pin antigo.
355
+ */
356
+ one.path.endsWith(".toml")
357
+ ? `✓ ${one.path.padEnd(22)} ${fmt(codexPinBefore()
358
+ ? "the tools moved from {was} to this one"
359
+ : "{n} tools the agent can ask", { was: codexPinBefore() ?? "", n: MCP_TOOL_COUNT })}`
360
+ : `✓ ${one.path.padEnd(22)} ${say("unpinned - it follows the published reader")}`
361
+ : `· ${one.path.padEnd(22)} ${say("already had it")}`);
299
362
  }
300
363
  /**
301
364
  * ONDE O BLOCO CAIU, DITO POR NOME.
@@ -311,7 +374,7 @@ export async function connect(opts) {
311
374
  */
312
375
  for (const home of await blockHomes(root)) {
313
376
  const moved = contract.changed.includes(home);
314
- console.log(body(`${moved ? "✓" : "·"} ${home.padEnd(22)} ${moved
377
+ row(moved, `${moved ? "✓" : "·"} ${home.padEnd(22)} ${moved
315
378
  ? home === "CLAUDE.md"
316
379
  ? /**
317
380
  * A FRASE DIZ O QUE O BLOCO CARREGA, e ela descrevia um conteúdo que não existia.
@@ -321,12 +384,12 @@ export async function connect(opts) {
321
384
  * fazer, e a linha diz qual dos dois ele é (dono, 21/08).
322
385
  */
323
386
  contract.count === 0
324
- ? "how to turn this repo into your system"
325
- : "rewritten for what is installed"
326
- : "the same rules, where this agent reads them"
387
+ ? say("how to turn this repo into your system")
388
+ : say("rewritten for what is installed")
389
+ : say("the same rules, where this agent reads")
327
390
  : contract.count === 0
328
- ? "already says how to start"
329
- : "already says what is installed"}`));
391
+ ? say("already says how to start")
392
+ : say("already says what is installed")}`);
330
393
  }
331
394
  /**
332
395
  * The fourth layer, and the one that had no installer at all: the import
@@ -369,21 +432,26 @@ export async function connect(opts) {
369
432
  if (had == null)
370
433
  continue;
371
434
  await rm(dir, { recursive: true, force: true }).catch(() => { });
372
- console.log(body(`✕ /${legacy.padEnd(21)} removed - renamed to /sui-import-ds`));
435
+ row(true, `✕ /${legacy.padEnd(21)} ${say("removed - renamed to /sui-import-ds")}`);
373
436
  }
437
+ /** Se ALGUMA skill mudou nesta rodada - é o que autoriza a frase da sessão nova a citá-las. */
438
+ let skillsMoved = false;
374
439
  for (const skill of skills) {
375
440
  const skillPath = join(root, skill.path);
376
441
  const before = await readFile(skillPath, "utf8").catch(() => null);
377
442
  if (before !== skill.source) {
378
443
  await mkdir(dirname(skillPath), { recursive: true });
379
444
  await writeFile(skillPath, skill.source, "utf8");
445
+ skillsMoved = true;
380
446
  }
381
- console.log(body(before === skill.source
382
- ? `· ${skill.label.padEnd(22)} already current`
447
+ row(before !== skill.source, before === skill.source
448
+ ? `· ${skill.label.padEnd(22)} ${say("already current")}`
383
449
  : before == null
384
- ? `✓ ${skill.label.padEnd(22)} ${skill.what}`
385
- : `✓ ${skill.label.padEnd(22)} updated to this CLI's pipeline`));
450
+ ? `✓ ${skill.label.padEnd(22)} ${say(skill.what)}`
451
+ : `✓ ${skill.label.padEnd(22)} ${say("updated to this CLI's pipeline")}`);
386
452
  }
453
+ /** A tela sai agora, junta: o que mudou por nome, e o silêncio numa linha. */
454
+ flush();
387
455
  /**
388
456
  * O CI: escrito quando pedido, e NÃO MAIS OFERECIDO aqui.
389
457
  *
@@ -419,9 +487,29 @@ export async function connect(opts) {
419
487
  const mcpMoved = Boolean(want.mcp &&
420
488
  Array.isArray(wired.mcp) &&
421
489
  wired.mcp.some((m) => moved(m.status)));
490
+ /**
491
+ * O QUE ESTA RODADA MOVEU, PELO NOME - e são os nomes que a pessoa acabou de ler nas linhas com
492
+ * `✓`, não os nossos nomes internos. Uma razão que ela pode conferir contra a própria tela.
493
+ */
494
+ const bothChecks = moved(wired.hook) && moved(wired.session);
495
+ const loads = [
496
+ /** Os dois checks juntos viram "the checks": nomeá-los separados custava uma linha inteira da
497
+ * tela para uma distinção que não muda nada do que a pessoa faz em seguida. */
498
+ ...(bothChecks
499
+ ? [say("the checks")]
500
+ : moved(wired.hook)
501
+ ? [say("the write check")]
502
+ : moved(wired.session)
503
+ ? [say("the session check")]
504
+ : []),
505
+ ...(mcpMoved ? [say("the tools")] : []),
506
+ ...(skillsMoved ? [say("the skills")] : []),
507
+ ];
422
508
  await reportWhatIsLeft(root, {
423
509
  cli: opts.version,
424
- ...(needsRestart ? { freshSession: { mcp: mcpMoved } } : {}),
510
+ ...(needsRestart || skillsMoved
511
+ ? { freshSession: { loads, mcp: mcpMoved } }
512
+ : {}),
425
513
  });
426
514
  /**
427
515
  * O CUSTO DO HOOK, DITO SEM UM NÚMERO QUE NÃO É NOSSO.
@@ -437,11 +525,20 @@ export async function connect(opts) {
437
525
  * que se sustenta.
438
526
  */
439
527
  if (wired.command.startsWith("npx synthesisui@")) {
528
+ /**
529
+ * O CUSTO DO HOOK, EM UMA LINHA - eram cinco, e elas fechavam a tela.
530
+ *
531
+ * Medido em 03/09: a tela do `connect` tinha 41 linhas, e este parágrafo era 5 delas, na última
532
+ * posição, falando de resolução de pacote npm. Para quem acabou de instalar, é o assunto mais
533
+ * distante do design system dele possível, no momento em que ele deveria estar abrindo o agente.
534
+ *
535
+ * A METADE QUE FICA É A ACIONÁVEL. O diagnóstico completo - por que o npx custa, o que foi
536
+ * medido, quanto o caminho local ganhou - vive na documentação do comando, guardado por
537
+ * `apps/web/src/lib/cli/commands.spec.ts`.
538
+ */
440
539
  console.log("");
441
- console.log(body(paint.dim("The hook runs through npx, which resolves this package against the")));
442
- console.log(body(paint.dim("registry on every edit - that wait is the network, not the check itself")));
443
- console.log(body(paint.dim("(the analysis is about 80ms). Adding synthesisui to your devDependencies")));
444
- console.log(body(paint.dim("makes npx resolve it locally instead, which is several times faster;")));
445
- console.log(body(paint.dim("run this again afterwards and it will switch by itself.")));
540
+ console.log(bodyWrapped(say("The hook resolves against the npm registry on each edit - that is the wait, not the check. Add synthesisui to your devDependencies and run this again; it switches by itself to local, several times faster."))
541
+ .map(paint.dim)
542
+ .join("\n"));
446
543
  }
447
544
  }
@@ -69,7 +69,19 @@ export async function gaps(opts) {
69
69
  const carried = await carriedByGlobals(root);
70
70
  if (carried) {
71
71
  console.log("");
72
- console.log(body(`${carried.total} of those declarations are already in your system - ${carried.parts}. They are not component recipes, which is why they count as "not interpreted" above; they did arrive.`));
72
+ console.log(body(
73
+ /**
74
+ * AS DUAS UNIDADES, DITAS - e ler a frase antiga era fazer uma conta errada.
75
+ *
76
+ * Ela dizia "24 of those declarations are already in your system - 6 base rules, 1 of your
77
+ * utilities, 1 registered property", e 6+1+1 é 8. Os números são certos e contam coisas
78
+ * DIFERENTES: `total` soma DECLARAÇÕES, `parts` conta as REGRAS e os UTILITÁRIOS em que elas
79
+ * moram. Quem lê subtrai e não fecha - e um número que não fecha derruba a confiança nos
80
+ * outros da mesma tela, que estão certos. Visto pelo dono no primeiro import real, 03/09.
81
+ *
82
+ * `across` é a palavra que carrega a mudança de unidade.
83
+ */
84
+ `${carried.total} of those declarations are already in your system - across ${carried.parts}. They are not component recipes, which is why they count as "not interpreted" above; they did arrive.`));
73
85
  }
74
86
  }
75
87
  /**
@@ -109,10 +121,10 @@ async function carriedByGlobals(root) {
109
121
  continue;
110
122
  const parts = [
111
123
  (globals.base ?? []).length > 0
112
- ? `${(globals.base ?? []).length} base rules`
124
+ ? `${(globals.base ?? []).length} base ${(globals.base ?? []).length === 1 ? "rule" : "rules"}`
113
125
  : "",
114
126
  Object.keys(globals.utilities ?? {}).length > 0
115
- ? `${Object.keys(globals.utilities ?? {}).length} of your utilities`
127
+ ? `${Object.keys(globals.utilities ?? {}).length} of your ${Object.keys(globals.utilities ?? {}).length === 1 ? "utilities entry" : "utilities"}`
116
128
  : "",
117
129
  props > 0
118
130
  ? `${props} registered ${props === 1 ? "property" : "properties"}`
@@ -1,5 +1,5 @@
1
1
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
- import { basename, dirname, join, relative, sep } from "node:path";
2
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
3
  import { anatomyFromSketch } from "../anatomy-from-sketch.js";
4
4
  import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
5
5
  import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
@@ -2771,6 +2771,11 @@ export async function takeCensus(root, opts) {
2771
2771
  * A primeira parece perfeita, e é isso que faz o defeito sobreviver. O `usage` estava dois campos
2772
2772
  * acima o tempo todo, gravado por este mesmo objeto - os dois chegam juntos e só um sobrevivia.
2773
2773
  */
2774
+ /**
2775
+ * A PROCEDÊNCIA, GRAVADA - ver `measured` no tipo. `basename` e não o caminho: o nome da pasta
2776
+ * distingue dois monorepos parecidos sem mandar o diretório pessoal dele para o nosso servidor.
2777
+ */
2778
+ measured: { repo: basename(resolve(root)), at: new Date().toISOString() },
2774
2779
  ...(scopeLabel ? { scope: scopeLabel } : {}),
2775
2780
  /**
2776
2781
  * O DENOMINADOR DO FORA - ver `outside-scope.ts`.
@@ -0,0 +1,104 @@
1
+ import { register } from "../lang.js";
2
+ /**
3
+ * A TELA DO `connect` EM PORTUGUÊS DO BRASIL - a primeira tela deste produto a falar outra língua.
4
+ *
5
+ * O QUE O CLIENTE GANHA: quem trabalha em português lê a nossa tela em português, sem uma pergunta
6
+ * a mais e sem misturar. O defeito que isto fecha foi medido no primeiro import real (03/09): o
7
+ * agente narra na língua da pessoa e os nossos blocos saíam em inglês, na mesma tela.
8
+ *
9
+ * ESTA TABELA É UMA PROMESSA COMPLETA, não um começo. `connect` está declarado em `COVERED` no
10
+ * `lang.ts`, e `connect-speaks.spec.ts` reprova se sobrar UMA frase da tela sem tradução aqui. É a
11
+ * diferença entre "traduzimos o connect" e "traduzimos parte do connect".
12
+ *
13
+ * AS TRÊS REGRAS QUE A TRADUÇÃO SEGUE, e nenhuma é estilística:
14
+ *
15
+ * 1. O NOME DA PEÇA NÃO TRADUZ. `hook`, `token`, `design system`, `commit` - são o vocabulário
16
+ * de quem trabalha com isto, e "gancho" obrigaria a pessoa a traduzir de volta para entender.
17
+ * O que traduz é a PROSA em volta deles.
18
+ *
19
+ * 2. CAMINHO, COMANDO E NÚMERO NÃO TRADUZEM. `.claude/settings.json` é o mesmo arquivo em toda
20
+ * língua, e um caminho traduzido é um caminho que não existe.
21
+ *
22
+ * 3. A LARGURA CONTINUA VALENDO. A coluna da lista dá 39 colunas depois do prefixo (ver
23
+ * `screen-fits.spec.ts`), e o português é tipicamente 15-20% mais longo que o inglês. Cada
24
+ * linha desta tabela foi medida, não traduzida e esquecida.
25
+ */
26
+ register("pt-BR", {
27
+ // ── as seções ──
28
+ Connected: "Conectado",
29
+ "Do this next": "Faça isto agora",
30
+ /** O último elo de uma lista - o resto da vírgula é igual nas duas línguas. */
31
+ "{head} and {last}": "{head} e {last}",
32
+ // ── o estado do repositório ──
33
+ "· no design system installed here yet - the agent has no index, and memory has nothing to belong to.": "· nenhum design system instalado aqui ainda - o agente não tem índice, e a memória não tem a que pertencer.",
34
+ // ── a fiação do agente ──
35
+ "✓ .claude/settings.json the check now runs after every write": "✓ .claude/settings.json a verificação roda após cada escrita",
36
+ "· .claude/settings.json already had it": "· .claude/settings.json já estava lá",
37
+ "✓ .claude/settings.json each session opens with what is missing": "✓ .claude/settings.json cada sessão abre com o que falta",
38
+ "✓ .claude/settings.json the session check moved to this one": "✓ .claude/settings.json a verificação de sessão veio para esta",
39
+ "· .claude/settings.json the session check was already there": "· .claude/settings.json a verificação de sessão já estava lá",
40
+ // ── o MCP e as skills ──
41
+ "{n} tools the agent can ask": "{n} ferramentas que o agente pode consultar",
42
+ "the tools moved from {was} to this one": "as ferramentas vieram da {was} para esta",
43
+ "unpinned - it follows the published reader": "solto - acompanha o leitor publicado",
44
+ "already had it": "já estava lá",
45
+ "already current": "já está em dia",
46
+ "updated to this CLI's pipeline": "atualizada para esta versão",
47
+ "removed - renamed to /sui-import-ds": "removida - virou /sui-import-ds",
48
+ // ── onde o bloco de regras caiu ──
49
+ "how to turn this repo into your system": "como transformar este repo no seu sistema",
50
+ "rewritten for what is installed": "reescrito para o que está instalado",
51
+ "the same rules, where this agent reads": "as mesmas regras, onde este agente lê",
52
+ "already says how to start": "já diz como começar",
53
+ "already says what is installed": "já diz o que está instalado",
54
+ /**
55
+ * O silêncio. "está em dia" é mais curto que "already current" e cabe com folga - a régua de
56
+ * largura vale igual aqui.
57
+ */
58
+ "· nothing to change - this environment is already current": "· nada a mudar - este ambiente já está em dia",
59
+ "· {n} other piece already current": "· mais {n} peça já em dia",
60
+ "· {n} other pieces already current": "· mais {n} peças já em dia",
61
+ // ── a oferta do terminal ──
62
+ "Your terminal can warn you when this repo drifts - one block in {rc}, silent unless something is wrong, at most once an hour.": "Seu terminal pode avisar quando este repo sair do sistema - um bloco em {rc}, calado a menos que algo esteja errado, no máximo uma vez por hora.",
63
+ // ── a ação recomendada ──
64
+ "and say": "e diga",
65
+ "Open a new agent session in this repo": "Abra uma sessão nova do agente neste repo",
66
+ /**
67
+ * A FRASE QUE ELE DIZ AO AGENTE **NÃO** TRADUZ, e é a decisão mais importante desta tabela.
68
+ *
69
+ * `import my design system` é o que ele digita, e as descrições das skills - que o agente lê para
70
+ * decidir qual usar - são em inglês. Traduzir a frase mandaria a pessoa dizer algo que o agente
71
+ * escolhe pior. É a mesma razão de `mcp.ts` nunca traduzir: onde a máquina lê, a precisão vence a
72
+ * gentileza. O dia em que as skills forem multilíngues, esta frase entra aqui.
73
+ */
74
+ "I read what is already in your code - the colours, the type, the shapes, the components. Nothing is invented.": "Eu leio o que já está no seu código - as cores, a tipografia, as formas, os componentes. Nada é inventado.",
75
+ "Drop the flag to approve each write yourself.": "Tire a flag para aprovar cada escrita você mesmo.",
76
+ "Drop the flags to approve each write yourself.": "Tire as flags para aprovar cada escrita você mesmo.",
77
+ /**
78
+ * `moveu` E NÃO `mexeu em` - e a razão é gramatical, não de gosto.
79
+ *
80
+ * Em português a preposição contrai com o artigo do que vem depois: "em" + "as verificações" é
81
+ * "NAS verificações", e "em" + "o hook" é "NO hook". O marcador `{what}` não sabe o gênero nem o
82
+ * número do que vai cair nele, então qualquer frase que ponha preposição antes dele sai errada em
83
+ * metade dos casos - saiu "mexeu em as verificações" na primeira medição. Um verbo transitivo
84
+ * direto resolve sem tabela de contração.
85
+ */
86
+ "This run moved {what}, and a new session is what reads it.": "Esta rodada moveu {what}, e é uma sessão nova que lê isso.",
87
+ "This run moved {what}, and a new session is what reads them.": "Esta rodada moveu {what}, e é uma sessão nova que lê isso.",
88
+ "A new session reads the wiring this run changed.": "Uma sessão nova lê a fiação que esta rodada mudou.",
89
+ "The project's tools ask for approval once - say yes.": "As ferramentas do projeto pedem aprovação uma vez - diga sim.",
90
+ // ── os nomes do que moveu, usados dentro da frase acima ──
91
+ "the checks": "as verificações",
92
+ "the write check": "a verificação de escrita",
93
+ "the session check": "a verificação de sessão",
94
+ "the tools": "as ferramentas",
95
+ "the skills": "as skills",
96
+ // ── o custo do hook ──
97
+ "The hook resolves against the npm registry on each edit - that is the wait, not the check. Add synthesisui to your devDependencies and run this again; it switches by itself to local, several times faster.": "O hook resolve no registro do npm a cada edição - a espera é isso, não a verificação. Adicione synthesisui às suas devDependencies e rode isto de novo; ele passa a resolver local sozinho, várias vezes mais rápido.",
98
+ // ── as descrições das skills ──
99
+ "the first run, start to finish": "o primeiro uso, do início ao fim",
100
+ "the import, orchestrated": "o import, orquestrado",
101
+ "one component against the system": "um componente contra o sistema",
102
+ "build what the system does not have": "construir o que o sistema não tem",
103
+ "the tokens, wired into your app": "os tokens, ligados no seu app",
104
+ });
@@ -77,7 +77,15 @@ function whereOf(shape, reason) {
77
77
  }
78
78
  : {
79
79
  module: null,
80
- work: "no module owns this shape - a reader here is a new module, and it needs an app in the corpus before it exists",
80
+ /**
81
+ * DITO PELO EFEITO NO PROJETO DELE, não pela nossa condição interna.
82
+ *
83
+ * Dizia "it needs an app in the corpus before it exists" - `corpus` é o nosso conjunto
84
+ * de medição, uma palavra que só existe dentro desta empresa, e ele não tem o que fazer
85
+ * com essa informação. O que ELE precisa saber é que esta forma ainda não tem leitor e
86
+ * que o pedido dele é o que a coloca na fila.
87
+ */
88
+ work: "nothing here reads this shape yet - ask for it and this file becomes the case that gets it built",
81
89
  };
82
90
  }
83
91
  case "value-gap":
@@ -188,7 +196,18 @@ values) {
188
196
  const lines = values
189
197
  ? [
190
198
  ...describeValueRuler(values),
191
- `The ${unread} style fragments not interpreted are sorted below by what to do about them.`,
199
+ /**
200
+ * A SEGUNDA FRASE CONTA OUTRA COISA, E PASSA A DIZER QUAL.
201
+ *
202
+ * A linha de cima fala em DECISÕES (1063, das quais 1005 interpretadas) e esta fala em
203
+ * FRAGMENTOS (32). Quem lê subtrai 1063-1005 = 58, vê 32 na linha seguinte, e as duas dizem
204
+ * "not interpreted". O comentário da régua única, logo acima, registra que esta tela já
205
+ * produziu uma segunda resposta uma vez - o percentual foi consertado, as CONTAGENS ficaram.
206
+ *
207
+ * Visto pelo dono no primeiro import real, 03/09. Um fragmento é um LUGAR onde a decisão
208
+ * mora, e é isso que a frase passa a dizer.
209
+ */
210
+ `They live in ${unread} ${unread === 1 ? "place" : "places"} in your code, sorted below by what to do about each.`,
192
211
  ]
193
212
  : [
194
213
  `${t.counted} style fragments accounted for, ${t.interpreted} interpreted (${t.percent}%). The ${unread} below are sorted by what to do about them.`,
@@ -197,7 +216,9 @@ values) {
197
216
  lines.push("", `MEASURED BY CLI ${t.measuredBy}, TRIAGED BY ${t.triagedBy}. Re-run \`import\` before concluding anything from the numbers below - a gap here may already be closed, and the work already done.`);
198
217
  }
199
218
  const label = {
200
- "no-reader": "NO READER FOR THIS SHAPE - this is work",
219
+ /** `this is work` era o nosso bilhete de triagem na tela dele - trabalho de quem? A faixa já
220
+ * diz o fato, e o que falta é dizer de quem é a vez: nossa. */
221
+ "no-reader": "NO READER FOR THIS SHAPE - ours to build",
201
222
  "value-gap": "THE SHAPE IS READ, THE VALUE IS NOT - a scale, not a reader",
202
223
  "by-design": "NOT A READING GAP - nothing a reader would change, and no work here",
203
224
  };
@@ -399,7 +399,7 @@ values) {
399
399
  * valor está inteiramente no sistema, e o que falta é só saber qual elemento o veste.
400
400
  */
401
401
  const theirs = g.withTheirTokens && g.withTheirTokens > 0
402
- ? ` — and ${g.withTheirTokens} of them already wear a token you declare, so their value is in the system`
402
+ ? ` - and ${g.withTheirTokens} of them already wear a token you declare, so their value is in the system`
403
403
  : "";
404
404
  lines.push(` ${g.uses} ${g.shape}${g.uses === 1 ? "" : "s"} in ${g.files} file${g.files === 1 ? "" : "s"} - ${g.because}${theirs}`);
405
405
  /**
@@ -417,7 +417,13 @@ values) {
417
417
  * do cliente e ainda não foi tomada.
418
418
  */
419
419
  if (g.distinct !== undefined && g.distinct > g.unreadable.length)
420
- lines.push(` of ${g.distinct} distinct texts here, ${g.unreadable.length} travel with the census - a reader we publish later reaches those without you re-measuring, and the remaining ${g.distinct - g.unreadable.length} need this scan run again`);
420
+ lines.push(
421
+ /**
422
+ * `census` ERA O NOSSO NOME NA TELA DELE - trocado por "the measurement we already have",
423
+ * que é o que a palavra significa do lado dele. Pego pela régua de `INV-VOC-05` no dia em
424
+ * que `doctor/` entrou nela (03/09).
425
+ */
426
+ ` of ${g.distinct} distinct texts here, ${g.unreadable.length} travel with the measurement we already have - a reader we publish later reaches those without you running this again, and the remaining ${g.distinct - g.unreadable.length} need another scan`);
421
427
  for (const e of g.examples)
422
428
  lines.push(` ${e.file}:${e.line} ${e.text.slice(0, 80)}`);
423
429
  }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { boolFlag, parseFlags } from "./cli-flags.js";
5
+ import "./copy/connect.pt-BR.js";
5
6
  import { absorb } from "./commands/absorb.js";
6
7
  import { add } from "./commands/add.js";
7
8
  import { adopt } from "./commands/adopt.js";
@@ -31,6 +32,7 @@ import { upgrade } from "./commands/upgrade.js";
31
32
  import { use } from "./commands/use.js";
32
33
  import { appendEvent } from "./doctor/ledger.js";
33
34
  import { blueprintTarget, installedSlugs } from "./installed.js";
35
+ import { useLang } from "./lang.js";
34
36
  import { RegistryError } from "./registry.js";
35
37
  /** Our own version, for pinning the hook and MCP commands we write into a
36
38
  * project. Read from the package we are running out of, so a pinned command
@@ -158,6 +160,14 @@ async function main() {
158
160
  }
159
161
  const registry = typeof flags.registry === "string" ? flags.registry : undefined;
160
162
  const dir = typeof flags.dir === "string" ? flags.dir : undefined;
163
+ /**
164
+ * A LÍNGUA DESTA EXECUÇÃO, ligada UMA vez e antes de qualquer impressão.
165
+ *
166
+ * `useLang` só ativa outra língua se ESTE comando tem a tela coberta inteira (ver `COVERED` em
167
+ * `lang.ts`) - um comando de fora roda em inglês mesmo com o locale em português, porque meia
168
+ * tela traduzida é o defeito que aquele módulo existe para não cometer.
169
+ */
170
+ useLang(command, process.env);
161
171
  switch (command) {
162
172
  case "import":
163
173
  // The census is arithmetic over their files; --dry keeps it on disk.
@@ -162,7 +162,15 @@
162
162
  * O que o cliente ganha ao rodar `upgrade`: as regras do sistema passam a existir no arquivo que o
163
163
  * agente DELE lê, em vez de só no do Claude Code.
164
164
  */
165
- export const MATERIALISER_SINCE = "0.16.360";
165
+ /**
166
+ * 0.16.360 -> 0.16.362 em 03/09: as ferramentas do sistema passam a ser fiadas no Codex. Num
167
+ * repositório com `.codex/`, `[mcp_servers.synthesisui]` nasce em `.codex/config.toml` - e é o
168
+ * `upgrade` que também chama `wireAgent`, então isto é ARQUIVO NOVO na pasta de quem já instalou.
169
+ *
170
+ * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
171
+ * sistema, em vez de só receber as regras e adivinhar o resto.
172
+ */
173
+ export const MATERIALISER_SINCE = "0.16.362";
166
174
  /**
167
175
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
168
176
  *
package/dist/lang.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * A LÍNGUA EM QUE A GENTE FALA COM ELE - DERIVADA, nunca perguntada.
3
+ *
4
+ * O QUE O CLIENTE GANHA: um comando cuja tela chega na língua dele, sem uma pergunta a mais no
5
+ * primeiro uso e sem uma bandeirinha para clicar.
6
+ *
7
+ * O DEFEITO QUE ISTO FECHA, medido no primeiro import real (03/09): o agente narra na língua da
8
+ * pessoa - porque é a língua em que ela conversa - e os nossos blocos saíam em inglês, na MESMA
9
+ * tela. Uma tela com duas línguas é a mesma classe de defeito que uma tela com duas instruções.
10
+ *
11
+ * DERIVAR, E NÃO PERGUNTAR, é decisão desta casa desde 27/07, escrita em `claude-md.ts`: *"THE
12
+ * INTERFACE LANGUAGE, READ FROM THE PROJECT RATHER THAN ASKED FOR"*. Aqui a fonte é OUTRA, e a
13
+ * diferença importa: aquela decide a língua do CÓDIGO GERADO e vem do projeto (`<html lang>`),
14
+ * porque o app é que tem público. Esta decide a língua da NOSSA TELA e vem da PESSOA - um
15
+ * desenvolvedor brasileiro constrói produto em inglês todo dia, e ler o `<html lang>` dele para
16
+ * escolher a língua do terminal erraria exatamente esse caso.
17
+ *
18
+ * A ORDEM, e o porquê de cada degrau:
19
+ *
20
+ * SUI_LANG a escolha explícita dele, que vence tudo - é a saída para quem tem locale
21
+ * de sistema numa língua e prefere ler noutra
22
+ * LC_ALL / LANG o que o sistema operacional dele já sabe. Ninguém precisa nos contar
23
+ * en o piso. Nunca uma língua adivinhada por IP, fuso ou nome de pasta
24
+ *
25
+ * NENHUMA TELA MISTURADA - e é isto que decide a arquitetura em vez do tamanho da tradução. A
26
+ * cobertura é por COMANDO, nunca por frase: um comando ou fala a língua dele inteiro, ou fala
27
+ * inglês inteiro. Traduzir por frase produziria a tela meio-e-meio que motivou este módulo, com
28
+ * a nossa assinatura em vez da coincidência.
29
+ *
30
+ * O QUE NUNCA TRADUZ, e a razão é dura: o que um AGENTE lê. As 11 ferramentas do MCP e as
31
+ * descrições que elas carregam são contrato de máquina - um modelo escolhe ferramenta lendo aquele
32
+ * texto, e traduzi-lo troca a precisão de uma escolha por uma gentileza que ninguém vê.
33
+ */
34
+ /**
35
+ * O que um locale de sistema quer dizer para nós - e só o que a gente sabe cumprir.
36
+ *
37
+ * `pt_BR.UTF-8`, `pt-BR`, `pt` e `pt_PT` todos caem em `pt-BR`, e isto é uma escolha declarada:
38
+ * a tradução foi escrita em português do Brasil. Um leitor de Portugal lê algo legível e não o
39
+ * seu próprio registro - e é melhor que inglês, mas o dia em que `pt-PT` for pedido ele entra
40
+ * como língua própria em vez de continuar escondido aqui.
41
+ */
42
+ function fromLocale(raw) {
43
+ const tag = raw.trim().toLowerCase().replace("_", "-").split(".")[0];
44
+ if (!tag)
45
+ return null;
46
+ if (tag.startsWith("pt"))
47
+ return "pt-BR";
48
+ if (tag.startsWith("en"))
49
+ return "en";
50
+ return null;
51
+ }
52
+ /**
53
+ * A LÍNGUA DESTE AMBIENTE. Pura: recebe o ambiente, nunca o lê de `process` por dentro - é o que
54
+ * permite um spec provar os quatro degraus sem mexer em variável global.
55
+ */
56
+ export function resolveLang(env) {
57
+ const explicit = env.SUI_LANG ? fromLocale(env.SUI_LANG) : null;
58
+ if (explicit)
59
+ return explicit;
60
+ for (const key of ["LC_ALL", "LC_MESSAGES", "LANG"]) {
61
+ const found = env[key] ? fromLocale(env[key]) : null;
62
+ if (found)
63
+ return found;
64
+ }
65
+ return "en";
66
+ }
67
+ /**
68
+ * OS COMANDOS COM TELA COMPLETA NA OUTRA LÍNGUA - e a lista é a promessa.
69
+ *
70
+ * Um comando fora dela roda em inglês mesmo com o locale em português, e isso é deliberado: meia
71
+ * tela traduzida é o defeito que este módulo existe para não cometer. A lista cresce por comando,
72
+ * com a tela inteira medida, e é ela que o `--help` pode citar no dia em que alguém perguntar
73
+ * "vocês falam português?".
74
+ */
75
+ const COVERED = {
76
+ en: new Set(),
77
+ "pt-BR": new Set(["connect"]),
78
+ };
79
+ /** A língua ativa desta execução - `en` até alguém declarar outra. */
80
+ let active = "en";
81
+ /**
82
+ * Liga a língua para ESTE comando, se e somente se a tela dele está coberta inteira.
83
+ *
84
+ * Chamado uma vez, no despacho. Devolve a língua que ficou ativa, para o chamador poder dizer.
85
+ */
86
+ export function useLang(command, env) {
87
+ /**
88
+ * NÃO EXISTE UM `reset` SEPARADO, e a ausência é deliberada: um comando fora da lista devolve o
89
+ * piso, então `useLang("", {})` já É o reset que um teste precisa. Um export que só o spec chama
90
+ * é o que o `reachable.spec` reprova, e com razão - ele vira código que ninguém mais mantém.
91
+ */
92
+ const wanted = resolveLang(env);
93
+ active = COVERED[wanted].has(command) ? wanted : "en";
94
+ return active;
95
+ }
96
+ /**
97
+ * A TRADUÇÃO, CHAVEADA PELA FRASE EM INGLÊS - e não por um identificador.
98
+ *
99
+ * O identificador (`connect.hook.added`) é a forma clássica e ela custa uma coisa que este
100
+ * repositório não pode pagar: a frase sai do lugar onde a decisão dela está escrita. Aqui cada
101
+ * frase mora ao lado do comentário que conta por que ela é aquela frase, e a chave é o próprio
102
+ * texto - então uma frase que muda de redação PERDE a tradução e cai no inglês, o que é o
103
+ * comportamento certo: melhor inglês correto que português de uma versão anterior da promessa.
104
+ */
105
+ const PT_BR = {};
106
+ /**
107
+ * A frase que sai na tela: a tradução quando existe, o original quando não.
108
+ *
109
+ * O fallback é o original, nunca uma chave crua nem um vazio - a pior tela possível é a que perde
110
+ * a informação por causa da nossa tabela.
111
+ */
112
+ export function say(text) {
113
+ if (active === "en")
114
+ return text;
115
+ return PT_BR[text] ?? text;
116
+ }
117
+ /**
118
+ * A FRASE COM NÚMERO DENTRO - e o marcador existe para a ORDEM DAS PALAVRAS ser livre.
119
+ *
120
+ * `the check moved from {was} to this one` em português é *"a verificação saiu da {was} para esta"*:
121
+ * o número muda de lugar. Traduzir pedaço por pedaço prenderia toda língua à sintaxe do inglês,
122
+ * que é o erro clássico de i18n feito com concatenação.
123
+ *
124
+ * A CHAVE É O TEMPLATE, com os marcadores dentro - a tabela guarda a frase inteira e cada língua
125
+ * decide onde cada número entra.
126
+ */
127
+ export function fmt(text, vars) {
128
+ return say(text).replace(/\{(\w+)\}/g, (whole, key) => key in vars ? String(vars[key]) : whole);
129
+ }
130
+ /** Registra as frases de uma tela. Chamado pelos módulos de tradução, um por comando. */
131
+ export function register(lang, table) {
132
+ if (lang !== "pt-BR")
133
+ return;
134
+ for (const [k, v] of Object.entries(table))
135
+ PT_BR[k] = v;
136
+ }
137
+ /**
138
+ * O QUE A TABELA NÃO ALCANÇA NESTA TELA - a conta que impede a mistura silenciosa.
139
+ *
140
+ * Um spec pergunta isto por comando: se sobrar frase sem tradução num comando declarado como
141
+ * coberto, o spec reprova. É a diferença entre uma promessa medida e uma intenção.
142
+ */
143
+ export function missing(texts) {
144
+ return texts.filter((t) => !(t in PT_BR));
145
+ }
package/dist/skills.js CHANGED
@@ -36,7 +36,7 @@ export const SKILLS = [
36
36
  path: ADAPT_SKILL_PATH,
37
37
  source: ADAPT_SKILL,
38
38
  label: "/sui-adapt",
39
- what: "one component against the system, and what to do about it",
39
+ what: "one component against the system",
40
40
  },
41
41
  /**
42
42
  * A DE CONSTRUÇÃO, e ela é a que responde ao pedido mais comum de todos:
@@ -52,7 +52,7 @@ export const SKILLS = [
52
52
  path: COMPOSE_SKILL_PATH,
53
53
  source: COMPOSE_SKILL,
54
54
  label: "/sui-compose",
55
- what: "build what the system does not have, layer by layer",
55
+ what: "build what the system does not have",
56
56
  },
57
57
  /**
58
58
  * A DE LIGAR O SISTEMA NO APP, e ela é a única aqui que fecha um passo que a esteira já sabia
@@ -67,6 +67,6 @@ export const SKILLS = [
67
67
  path: CONFIGURE_SKILL_PATH,
68
68
  source: CONFIGURE_SKILL,
69
69
  label: "/sui-configure-ds",
70
- what: "the system wired into the app, so the tokens reach the browser",
70
+ what: "the tokens, wired into your app",
71
71
  },
72
72
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.360",
3
+ "version": "0.16.362",
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": {