synthesisui 0.16.361 → 0.16.363
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.
- package/dist/agent-wiring.js +49 -0
- package/dist/codex-mcp.js +85 -0
- package/dist/commands/align.js +20 -12
- package/dist/commands/connect.js +43 -31
- package/dist/commands/gaps.js +60 -5
- package/dist/commands/import.js +6 -1
- package/dist/copy/connect.pt-BR.js +104 -0
- package/dist/doctor/gap-triage.js +24 -3
- package/dist/doctor/style-ledger.js +8 -2
- package/dist/index.js +12 -0
- package/dist/install-marks.js +9 -1
- package/dist/lang.js +145 -0
- package/package.json +1 -1
package/dist/agent-wiring.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/align.js
CHANGED
|
@@ -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,40 +657,47 @@ 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 ?
|
|
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
|
-
? "Drop the flag to approve each write yourself."
|
|
680
|
-
: "Drop the flags to approve each write yourself.").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));
|
|
681
682
|
if (freshSession) {
|
|
682
683
|
const list = freshSession.loads;
|
|
683
684
|
const said = list.length === 0
|
|
684
685
|
? "and"
|
|
685
686
|
: list.length === 1
|
|
686
687
|
? list[0]
|
|
687
|
-
:
|
|
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
|
+
});
|
|
688
694
|
lines.push(...bodyWrapped(list.length === 0
|
|
689
|
-
? "A new session reads the wiring this run changed."
|
|
690
|
-
:
|
|
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));
|
|
691
699
|
if (freshSession.mcp)
|
|
692
|
-
lines.push(...bodyWrapped("The project's tools ask for approval once - say yes.").map(paint.dim));
|
|
700
|
+
lines.push(...bodyWrapped(say("The project's tools ask for approval once - say yes.")).map(paint.dim));
|
|
693
701
|
}
|
|
694
702
|
return lines.join("\n");
|
|
695
703
|
}
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 { fmt, say } from "../lang.js";
|
|
6
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";
|
|
@@ -178,7 +179,7 @@ version) {
|
|
|
178
179
|
* recomendada, empurrando para baixo a única coisa que a pessoa procurava. O que ela precisa dizer
|
|
179
180
|
* é o que ganha, o que a gente escreve e onde: nada disso saiu.
|
|
180
181
|
*/
|
|
181
|
-
console.log(bodyWrapped(
|
|
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"));
|
|
182
183
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
183
184
|
console.log(body(paint.blue(" npx synthesisui@latest connect --shell")));
|
|
184
185
|
return;
|
|
@@ -231,7 +232,7 @@ export async function connect(opts) {
|
|
|
231
232
|
// The block reads the settings we just wrote, so it must be regenerated
|
|
232
233
|
// after them, not before.
|
|
233
234
|
const contract = await syncClaudeMd(root);
|
|
234
|
-
console.log(section("Connected"));
|
|
235
|
+
console.log(section(say("Connected")));
|
|
235
236
|
/**
|
|
236
237
|
* A LISTA DA TELA, JUNTADA ANTES DE IMPRIMIR - e é isto que faz a rodada silenciosa caber numa
|
|
237
238
|
* linha.
|
|
@@ -260,8 +261,10 @@ export async function connect(opts) {
|
|
|
260
261
|
if (quiet === 0)
|
|
261
262
|
return;
|
|
262
263
|
console.log(body(paint.dim(quiet === rows.length
|
|
263
|
-
? "· nothing to change - this environment is already current"
|
|
264
|
-
:
|
|
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 }))));
|
|
265
268
|
};
|
|
266
269
|
/**
|
|
267
270
|
* E O QUE FALTA, DITO EM VOZ ALTA - porque este comando estava mentindo por omissão.
|
|
@@ -294,7 +297,7 @@ export async function connect(opts) {
|
|
|
294
297
|
*/
|
|
295
298
|
const anyInstalled = (await installedSlugs(root).catch(() => [])).length > 0;
|
|
296
299
|
if (!anyInstalled)
|
|
297
|
-
console.log(bodyWrapped("· no design system installed here yet - the agent has no index, and memory has nothing to belong to.").join("\n"));
|
|
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"));
|
|
298
301
|
/**
|
|
299
302
|
* O QUE ESTE CLI REESCREVEU NA PASTA DO SISTEMA - dito primeiro, porque é o que a pessoa não sabia
|
|
300
303
|
* que estava devendo. Ela rodou `connect` para atualizar a fiação; os arquivos do install estarem
|
|
@@ -307,7 +310,7 @@ export async function connect(opts) {
|
|
|
307
310
|
if (want.hook) {
|
|
308
311
|
row(wired.hook !== "already there", (() => {
|
|
309
312
|
return wired.hook === "added"
|
|
310
|
-
? "✓ .claude/settings.json the check now runs after every write"
|
|
313
|
+
? say("✓ .claude/settings.json the check now runs after every write")
|
|
311
314
|
: wired.hook === "updated"
|
|
312
315
|
? /**
|
|
313
316
|
* A LINHA QUE FALTAVA, e a ausência dela fazia o comando mentir: rodando o 0.16.157, a
|
|
@@ -315,7 +318,7 @@ export async function connect(opts) {
|
|
|
315
318
|
* (dono, 06/08). Quem lê "already had it" fecha o terminal.
|
|
316
319
|
*/
|
|
317
320
|
`✓ .claude/settings.json the check moved from ${wired.was?.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? "an older version"} to this one`
|
|
318
|
-
: "· .claude/settings.json already had it";
|
|
321
|
+
: say("· .claude/settings.json already had it");
|
|
319
322
|
})(), wired.command);
|
|
320
323
|
/**
|
|
321
324
|
* A SEGUNDA COSTURA, dita por nome. As onze ferramentas MCP são PULL e o hook de escrita roda
|
|
@@ -324,12 +327,12 @@ export async function connect(opts) {
|
|
|
324
327
|
*/
|
|
325
328
|
if (wired.session !== "skipped")
|
|
326
329
|
row(wired.session !== "already there", wired.session === "already there"
|
|
327
|
-
? "· .claude/settings.json the session check was already there"
|
|
330
|
+
? say("· .claude/settings.json the session check was already there")
|
|
328
331
|
: wired.session === "updated"
|
|
329
332
|
? /** A distinção `added` x `updated` vale desde 06/08: um estado que não se
|
|
330
333
|
* distingue de "nada a fazer" faz a instrução de atualizar mentir. */
|
|
331
|
-
"✓ .claude/settings.json the session check moved to this one"
|
|
332
|
-
: "✓ .claude/settings.json each session opens with what is missing");
|
|
334
|
+
say("✓ .claude/settings.json the session check moved to this one")
|
|
335
|
+
: say("✓ .claude/settings.json each session opens with what is missing"));
|
|
333
336
|
}
|
|
334
337
|
/**
|
|
335
338
|
* OS CAMINHOS DE MCP, POR NOME - e o número vem da LISTA, não da minha memória.
|
|
@@ -341,12 +344,21 @@ export async function connect(opts) {
|
|
|
341
344
|
if (want.mcp && wired.mcp !== "skipped") {
|
|
342
345
|
for (const one of wired.mcp)
|
|
343
346
|
row(one.status !== "already there", one.status === "added"
|
|
344
|
-
? `✓ ${one.path.padEnd(22)} ${
|
|
347
|
+
? `✓ ${one.path.padEnd(22)} ${fmt("{n} tools the agent can ask", { n: MCP_TOOL_COUNT })}`
|
|
345
348
|
: one.status === "updated"
|
|
346
|
-
? /**
|
|
347
|
-
*
|
|
348
|
-
|
|
349
|
-
|
|
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")}`);
|
|
350
362
|
}
|
|
351
363
|
/**
|
|
352
364
|
* ONDE O BLOCO CAIU, DITO POR NOME.
|
|
@@ -372,12 +384,12 @@ export async function connect(opts) {
|
|
|
372
384
|
* fazer, e a linha diz qual dos dois ele é (dono, 21/08).
|
|
373
385
|
*/
|
|
374
386
|
contract.count === 0
|
|
375
|
-
? "how to turn this repo into your system"
|
|
376
|
-
: "rewritten for what is installed"
|
|
377
|
-
: "the same rules, where this agent reads"
|
|
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")
|
|
378
390
|
: contract.count === 0
|
|
379
|
-
? "already says how to start"
|
|
380
|
-
: "already says what is installed"}`);
|
|
391
|
+
? say("already says how to start")
|
|
392
|
+
: say("already says what is installed")}`);
|
|
381
393
|
}
|
|
382
394
|
/**
|
|
383
395
|
* The fourth layer, and the one that had no installer at all: the import
|
|
@@ -420,7 +432,7 @@ export async function connect(opts) {
|
|
|
420
432
|
if (had == null)
|
|
421
433
|
continue;
|
|
422
434
|
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
423
|
-
row(true, `✕ /${legacy.padEnd(21)} removed - renamed to /sui-import-ds`);
|
|
435
|
+
row(true, `✕ /${legacy.padEnd(21)} ${say("removed - renamed to /sui-import-ds")}`);
|
|
424
436
|
}
|
|
425
437
|
/** Se ALGUMA skill mudou nesta rodada - é o que autoriza a frase da sessão nova a citá-las. */
|
|
426
438
|
let skillsMoved = false;
|
|
@@ -433,10 +445,10 @@ export async function connect(opts) {
|
|
|
433
445
|
skillsMoved = true;
|
|
434
446
|
}
|
|
435
447
|
row(before !== skill.source, before === skill.source
|
|
436
|
-
? `· ${skill.label.padEnd(22)} already current`
|
|
448
|
+
? `· ${skill.label.padEnd(22)} ${say("already current")}`
|
|
437
449
|
: before == null
|
|
438
|
-
? `✓ ${skill.label.padEnd(22)} ${skill.what}`
|
|
439
|
-
: `✓ ${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")}`);
|
|
440
452
|
}
|
|
441
453
|
/** A tela sai agora, junta: o que mudou por nome, e o silêncio numa linha. */
|
|
442
454
|
flush();
|
|
@@ -484,14 +496,14 @@ export async function connect(opts) {
|
|
|
484
496
|
/** Os dois checks juntos viram "the checks": nomeá-los separados custava uma linha inteira da
|
|
485
497
|
* tela para uma distinção que não muda nada do que a pessoa faz em seguida. */
|
|
486
498
|
...(bothChecks
|
|
487
|
-
? ["the checks"]
|
|
499
|
+
? [say("the checks")]
|
|
488
500
|
: moved(wired.hook)
|
|
489
|
-
? ["the write check"]
|
|
501
|
+
? [say("the write check")]
|
|
490
502
|
: moved(wired.session)
|
|
491
|
-
? ["the session check"]
|
|
503
|
+
? [say("the session check")]
|
|
492
504
|
: []),
|
|
493
|
-
...(mcpMoved ? ["the tools"] : []),
|
|
494
|
-
...(skillsMoved ? ["the skills"] : []),
|
|
505
|
+
...(mcpMoved ? [say("the tools")] : []),
|
|
506
|
+
...(skillsMoved ? [say("the skills")] : []),
|
|
495
507
|
];
|
|
496
508
|
await reportWhatIsLeft(root, {
|
|
497
509
|
cli: opts.version,
|
|
@@ -525,7 +537,7 @@ export async function connect(opts) {
|
|
|
525
537
|
* `apps/web/src/lib/cli/commands.spec.ts`.
|
|
526
538
|
*/
|
|
527
539
|
console.log("");
|
|
528
|
-
console.log(bodyWrapped("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.")
|
|
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."))
|
|
529
541
|
.map(paint.dim)
|
|
530
542
|
.join("\n"));
|
|
531
543
|
}
|
package/dist/commands/gaps.js
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
import { readdir, readFile } from "node:fs/promises";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { describeTriage, triageLedger } from "../doctor/gap-triage.js";
|
|
18
|
-
import { body, section } from "../output.js";
|
|
18
|
+
import { body, bodyWrapped, paint, section } from "../output.js";
|
|
19
19
|
export async function gaps(opts) {
|
|
20
20
|
const root = opts.dir ?? process.cwd();
|
|
21
21
|
const path = opts.census ?? join(root, "_synthesisui", "census.json");
|
|
22
22
|
const raw = await readFile(path, "utf8").catch(() => null);
|
|
23
|
-
console.log(section("What this pipeline did not read"));
|
|
23
|
+
console.log(section(opts.brief ? "What we read" : "What this pipeline did not read"));
|
|
24
24
|
if (!raw) {
|
|
25
25
|
console.log(body(`No measurement at ${path}. Run \`synthesisui import --dry\` first - that is what measures your files, and it writes the result there.`));
|
|
26
26
|
return;
|
|
@@ -46,6 +46,8 @@ export async function gaps(opts) {
|
|
|
46
46
|
console.log(body(`This measurement was written by a tool version that did not count style fragments yet, so the numbers do not exist rather than being zero. Run \`synthesisui import --dry\` again with ${opts.cli} to measure.`));
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
+
if (opts.brief)
|
|
50
|
+
return brief(ledger, values);
|
|
49
51
|
for (const line of describeTriage(triageLedger(ledger, opts.cli), values))
|
|
50
52
|
console.log(body(line));
|
|
51
53
|
/**
|
|
@@ -69,7 +71,19 @@ export async function gaps(opts) {
|
|
|
69
71
|
const carried = await carriedByGlobals(root);
|
|
70
72
|
if (carried) {
|
|
71
73
|
console.log("");
|
|
72
|
-
console.log(body(
|
|
74
|
+
console.log(body(
|
|
75
|
+
/**
|
|
76
|
+
* AS DUAS UNIDADES, DITAS - e ler a frase antiga era fazer uma conta errada.
|
|
77
|
+
*
|
|
78
|
+
* Ela dizia "24 of those declarations are already in your system - 6 base rules, 1 of your
|
|
79
|
+
* utilities, 1 registered property", e 6+1+1 é 8. Os números são certos e contam coisas
|
|
80
|
+
* DIFERENTES: `total` soma DECLARAÇÕES, `parts` conta as REGRAS e os UTILITÁRIOS em que elas
|
|
81
|
+
* moram. Quem lê subtrai e não fecha - e um número que não fecha derruba a confiança nos
|
|
82
|
+
* outros da mesma tela, que estão certos. Visto pelo dono no primeiro import real, 03/09.
|
|
83
|
+
*
|
|
84
|
+
* `across` é a palavra que carrega a mudança de unidade.
|
|
85
|
+
*/
|
|
86
|
+
`${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
87
|
}
|
|
74
88
|
}
|
|
75
89
|
/**
|
|
@@ -79,6 +93,47 @@ export async function gaps(opts) {
|
|
|
79
93
|
* vazia. Em qualquer um dos três não há nada a dizer, e uma frase que aparece sempre é uma frase que
|
|
80
94
|
* ninguém lê.
|
|
81
95
|
*/
|
|
96
|
+
/**
|
|
97
|
+
* O RESULTADO E UMA AÇÃO - o fechamento de um import.
|
|
98
|
+
*
|
|
99
|
+
* TRÊS DECISÕES DE DESENHO, e cada uma responde a algo que o dono viu na tela em 03/09:
|
|
100
|
+
*
|
|
101
|
+
* O PERCENTUAL EM AZUL. `paint.blue` é a cor que este produto reserva para o que está VIVO - a
|
|
102
|
+
* barra de cobertura, os comandos, os links. A cobertura é o número que resume a entrega, e num
|
|
103
|
+
* bloco de prosa cinza ela desaparecia ("deixe com um tom de outra cor").
|
|
104
|
+
*
|
|
105
|
+
* A LACUNA EM UMA LINHA, COM ENDEREÇO. Ela continua dita - quantos fragmentos, em quantos
|
|
106
|
+
* lugares - e a LISTA mora no comando sem `--brief`. Uma lacuna sem número seria silêncio; uma
|
|
107
|
+
* lista no momento da entrega é pedir desculpa quando era hora de mostrar o resultado.
|
|
108
|
+
*
|
|
109
|
+
* UMA CTA, COM O QUE ELA GANHA. O v2 já existe como rascunho; o que faltava era dizer o que
|
|
110
|
+
* aprová-lo FAZ - agrupar cores quase iguais, nomear valor que carrega peso e não tem nome,
|
|
111
|
+
* resolver o valor que carrega dois nomes dele. Um link sem consequência dita é um link que
|
|
112
|
+
* espera para sempre.
|
|
113
|
+
*/
|
|
114
|
+
async function brief(ledger, values) {
|
|
115
|
+
const places = ledger.unread.length;
|
|
116
|
+
if (!values)
|
|
117
|
+
return;
|
|
118
|
+
const decisions = values.seen - values.structure;
|
|
119
|
+
const closed = values.interpreted + values.answered;
|
|
120
|
+
const pct = decisions > 0 ? Math.round((closed / decisions) * 100) : 0;
|
|
121
|
+
console.log(bodyWrapped(`${paint.blue(paint.strong(`${pct}%`))} of your design decisions are in the system - ${closed} of ${decisions}.`).join("\n"));
|
|
122
|
+
/**
|
|
123
|
+
* O QUE FALTA, NA MESMA UNIDADE DA LINHA DE CIMA - e a primeira versão desta função errou
|
|
124
|
+
* exatamente nisso.
|
|
125
|
+
*
|
|
126
|
+
* Ela dizia "222 fragments are not", ao lado de "920 de 1008": quem lê subtrai 88 e vê 222. É o
|
|
127
|
+
* mesmo defeito de duas unidades vizinhas que este PR conserta duas linhas acima, e eu o
|
|
128
|
+
* reintroduzi na frase nova. O resto vai em PONTOS PERCENTUAIS, que é a unidade que a pessoa
|
|
129
|
+
* acabou de ler, e o número de LUGARES é endereço e não contagem concorrente.
|
|
130
|
+
*
|
|
131
|
+
* E cala em 100%: "the other 0%" é uma linha que faz o leitor procurar o que não existe.
|
|
132
|
+
*/
|
|
133
|
+
if (pct >= 100 || places === 0)
|
|
134
|
+
return;
|
|
135
|
+
console.log(bodyWrapped(paint.dim(`The other ${100 - pct}% sits in ${places} ${places === 1 ? "place" : "places"} - \`synthesisui gaps\` lists each one with the file and the line.`)).join("\n"));
|
|
136
|
+
}
|
|
82
137
|
async function carriedByGlobals(root) {
|
|
83
138
|
const dir = join(root, "_synthesisui", "ds");
|
|
84
139
|
const slugs = await readdir(dir).catch(() => []);
|
|
@@ -109,10 +164,10 @@ async function carriedByGlobals(root) {
|
|
|
109
164
|
continue;
|
|
110
165
|
const parts = [
|
|
111
166
|
(globals.base ?? []).length > 0
|
|
112
|
-
? `${(globals.base ?? []).length} base rules`
|
|
167
|
+
? `${(globals.base ?? []).length} base ${(globals.base ?? []).length === 1 ? "rule" : "rules"}`
|
|
113
168
|
: "",
|
|
114
169
|
Object.keys(globals.utilities ?? {}).length > 0
|
|
115
|
-
? `${Object.keys(globals.utilities ?? {}).length} of your utilities`
|
|
170
|
+
? `${Object.keys(globals.utilities ?? {}).length} of your ${Object.keys(globals.utilities ?? {}).length === 1 ? "utilities entry" : "utilities"}`
|
|
116
171
|
: "",
|
|
117
172
|
props > 0
|
|
118
173
|
? `${props} registered ${props === 1 ? "property" : "properties"}`
|
package/dist/commands/import.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
? `
|
|
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(
|
|
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.
|
|
@@ -337,6 +347,8 @@ async function main() {
|
|
|
337
347
|
*/
|
|
338
348
|
await gaps({
|
|
339
349
|
dir,
|
|
350
|
+
/** `--brief`: o fechamento de um import - o resultado e UMA ação. Ver `gaps.ts`. */
|
|
351
|
+
...(boolFlag(flags, "brief") ? { brief: true } : {}),
|
|
340
352
|
census: typeof flags.census === "string" ? flags.census : undefined,
|
|
341
353
|
cli: CLI_VERSION,
|
|
342
354
|
});
|
package/dist/install-marks.js
CHANGED
|
@@ -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
|
-
|
|
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/package.json
CHANGED