synthesisui 0.16.428 → 0.16.430
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-md.js +19 -4
- package/dist/commands/add.js +12 -1
- package/dist/commands/import.js +154 -8
- package/dist/commands/mcp.js +15 -1
- package/dist/commands/sync.js +62 -17
- package/dist/install-marks.js +1 -1
- package/dist/report-html.js +205 -0
- package/dist/transcript.js +50 -0
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -184,14 +184,29 @@ best reasoning you produce.`;
|
|
|
184
184
|
* A checagem é LOCAL e grátis: o censo grava qual CLI o mediu (`ledger.cli`) e o servidor MCP É o CLI,
|
|
185
185
|
* então comparar as duas versões é aritmética em disco. Nada aqui telefona para casa - é a mesma
|
|
186
186
|
* decisão que mantém o hook instalado.
|
|
187
|
+
*
|
|
188
|
+
* O TEXTO DEIXOU DE DIZER "IT COSTS NOTHING" em 16/09, e a correção é de fidelidade, não de estilo.
|
|
189
|
+
* Medido: `remeasure` não recusa por versão - ele mede e ENVIA em toda chamada, e a comparação de
|
|
190
|
+
* versões só imprime um aviso. Então a frase antiga descrevia um comportamento que o código nunca
|
|
191
|
+
* teve, e a cota diária do plano grátis transformou essa imprecisão em consequência: o agente
|
|
192
|
+
* gastaria o teto da pessoa seguindo uma instrução nossa, escrita no repositório dela, dizendo que
|
|
193
|
+
* não custava nada.
|
|
194
|
+
*
|
|
195
|
+
* O que o bloco diz agora é o que a régua do plano diz: PERGUNTAR é livre, ENVIAR tem teto - e
|
|
196
|
+
* quando o teto chega, o agente repassa a frase em vez de tentar de novo.
|
|
187
197
|
*/
|
|
188
198
|
const REFRESH = `
|
|
189
199
|
|
|
190
200
|
**Call \`refresh_system\` once before you write UI here, or before you describe what
|
|
191
|
-
this system holds
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
this system holds** - once per session, not once per file, and never in a loop.
|
|
202
|
+
|
|
203
|
+
It is an UPDATE and not a question: it re-measures this repository and sends it. On
|
|
204
|
+
the free plan that counts against the updates the plan covers per day, and when the
|
|
205
|
+
day's updates are used up the tool says so and nothing is lost - the measurement
|
|
206
|
+
stays on disk, and every question you ask about this system keeps being answered.
|
|
207
|
+
Asking is always free; it is sending that has a ceiling. If it says the ceiling was
|
|
208
|
+
reached, say that to the person and carry on with what you already know - do not
|
|
209
|
+
retry it.
|
|
195
210
|
|
|
196
211
|
It re-measures this repo with the current reader, reuses what was already written
|
|
197
212
|
about this app, and sends it - a newer reader sees styles the old one could not, and
|
package/dist/commands/add.js
CHANGED
|
@@ -47,6 +47,12 @@ const IGNORED = [
|
|
|
47
47
|
/** O que a última medição viu, para a próxima dizer o que mudou - descreve ESTE clone. */
|
|
48
48
|
".last-sync.json",
|
|
49
49
|
"not-expressed.md",
|
|
50
|
+
/**
|
|
51
|
+
* O RELATÓRIO QUE O `inspect` ESCREVE - a leitura inteira com forma, para ele abrir no navegador.
|
|
52
|
+
* Mesma natureza do censo: é a MEDIÇÃO, reescrita por inteiro a cada rodada, então versioná-la é
|
|
53
|
+
* conflito toda vez e ainda poria um HTML de meio megabyte no diff de quem só rodou o comando.
|
|
54
|
+
*/
|
|
55
|
+
"report.html",
|
|
50
56
|
/**
|
|
51
57
|
* O MODO DELA - ver `mode.ts`. Commitado, ele barraria o time inteiro porque UMA pessoa ligou
|
|
52
58
|
* o bloqueio dela, e o aceite da etapa 06 é literal: *"a escolha de uma pessoa nunca barra
|
|
@@ -78,7 +84,12 @@ export async function ensureGovernanceHome(projectRoot) {
|
|
|
78
84
|
await mkdir(join(projectRoot, "_synthesisui"), { recursive: true });
|
|
79
85
|
await writeGovernanceIgnore(projectRoot);
|
|
80
86
|
}
|
|
81
|
-
|
|
87
|
+
/**
|
|
88
|
+
* EXPORTADO desde 18/09 porque o `inspect` passou a escrever um arquivo na pasta: o relatório. Um
|
|
89
|
+
* comando de primeiro contato que deixa um HTML de 30 KB aparecendo no `git status` de quem só quis
|
|
90
|
+
* olhar cobra da pessoa uma limpeza que ela não pediu - e a pasta já é declarada gerenciada aqui.
|
|
91
|
+
*/
|
|
92
|
+
export async function writeGovernanceIgnore(projectRoot) {
|
|
82
93
|
const path = join(projectRoot, "_synthesisui", ".gitignore");
|
|
83
94
|
const existing = await readFile(path, "utf8").catch(() => null);
|
|
84
95
|
/**
|
package/dist/commands/import.js
CHANGED
|
@@ -55,12 +55,14 @@ import { body, bodyWrapped, paint, section } from "../output.js";
|
|
|
55
55
|
import { consumersOf, outsideScope, } from "../outside-scope.js";
|
|
56
56
|
import { phase, startProgress } from "../progress.js";
|
|
57
57
|
import { repoStateOf } from "../repo-state.js";
|
|
58
|
+
import { relatorioHtml, secoesDe } from "../report-html.js";
|
|
58
59
|
import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
|
|
59
60
|
import { detectStack, resolveDeps, stackVersions } from "../stack.js";
|
|
61
|
+
import { desligarCaptura, ligarCaptura } from "../transcript.js";
|
|
60
62
|
import { wiredSlugs, wiringWarning } from "../wired-slugs.js";
|
|
61
63
|
import { harvestWorkspaceCss } from "../workspace-css.js";
|
|
62
64
|
import { placeInWorkspace } from "../workspace-place.js";
|
|
63
|
-
import { add } from "./add.js";
|
|
65
|
+
import { add, writeGovernanceIgnore } from "./add.js";
|
|
64
66
|
import { walk, walkAll } from "./doctor.js";
|
|
65
67
|
/**
|
|
66
68
|
* UMA PÁGINA DELE, como ele a montou - ver `Census.pages`.
|
|
@@ -3720,6 +3722,145 @@ export function defaultScheme(declared) {
|
|
|
3720
3722
|
return dark > light ? "dark" : "light";
|
|
3721
3723
|
}
|
|
3722
3724
|
/** On a dry run there is nobody to ask, so say what was measured. */
|
|
3725
|
+
/**
|
|
3726
|
+
* A TELA DO `inspect`, E O ARQUIVO QUE CARREGA O RESTO (dono, 18/09).
|
|
3727
|
+
*
|
|
3728
|
+
* O QUE O CLIENTE GANHA: ele decide sem rolar. Ficam na tela os quatro blocos que MUDAM o que ele
|
|
3729
|
+
* vai fazer em seguida - quanto foi lido, se o root tem mais de um app, o que não resolve, e onde
|
|
3730
|
+
* está a leitura inteira. Medido na saída dele em 18/09: 250 linhas, 94 delas a mesma frase sobre
|
|
3731
|
+
* nome de parte. Nada se perde: o texto inteiro está no relatório, escrito pela MESMA esteira.
|
|
3732
|
+
*
|
|
3733
|
+
* A FRASE DO CONTRATO MUDA, E É A ÚNICA COISA QUE MUDA: era "nothing was written, and nothing was
|
|
3734
|
+
* sent". Passa a dizer a verdade - nada enviado, um arquivo escrito, e aqui está ele. Uma frase que
|
|
3735
|
+
* promete silêncio enquanto o comando escreve um arquivo é pior que o arquivo.
|
|
3736
|
+
*/
|
|
3737
|
+
async function mostrarInspecao(c, root, transcrito) {
|
|
3738
|
+
const secoes = secoesDe(transcrito);
|
|
3739
|
+
const valores = c.values?.classes;
|
|
3740
|
+
const decisoes = valores ? valores.seen - valores.structure : 0;
|
|
3741
|
+
const pct = valores && decisoes > 0
|
|
3742
|
+
? Math.round((valores.interpreted / decisoes) * 100)
|
|
3743
|
+
: null;
|
|
3744
|
+
const lidos = c.coverage?.read ?? 0;
|
|
3745
|
+
const total = c.coverage?.components ?? 0;
|
|
3746
|
+
const arquivo = join(root, "_synthesisui", "report.html");
|
|
3747
|
+
const html = () => relatorioHtml({
|
|
3748
|
+
cabecalho: {
|
|
3749
|
+
repo: c.measured?.repo ?? basename(root),
|
|
3750
|
+
medidoEm: c.measured?.at ?? "",
|
|
3751
|
+
cli: c.ledger?.cli ?? "",
|
|
3752
|
+
},
|
|
3753
|
+
destaques: [
|
|
3754
|
+
{
|
|
3755
|
+
numero: `${lidos} of ${total}`,
|
|
3756
|
+
rotulo: "components came out with a blueprint",
|
|
3757
|
+
},
|
|
3758
|
+
...(pct === null
|
|
3759
|
+
? []
|
|
3760
|
+
: [
|
|
3761
|
+
{
|
|
3762
|
+
numero: `${pct}%`,
|
|
3763
|
+
rotulo: `of ${decisoes} style decisions carry your vocabulary`,
|
|
3764
|
+
},
|
|
3765
|
+
]),
|
|
3766
|
+
{
|
|
3767
|
+
numero: String(c.brokenRefs?.length ?? 0),
|
|
3768
|
+
rotulo: "references that resolve to nothing",
|
|
3769
|
+
},
|
|
3770
|
+
],
|
|
3771
|
+
secoes,
|
|
3772
|
+
});
|
|
3773
|
+
console.log(section("What came out of your files"));
|
|
3774
|
+
console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
|
|
3775
|
+
? ""
|
|
3776
|
+
: `, and ${paint.strong(`${pct}%`)} of your ${decisoes} style decisions carry your own vocabulary`}.`));
|
|
3777
|
+
/**
|
|
3778
|
+
* OS DOIS BLOCOS QUE MUDAM O QUE ELE FAZ EM SEGUIDA, e eles saem do transcrito em vez de serem
|
|
3779
|
+
* reescritos: a régua que os produziu é a mesma, então a tela nunca diverge do relatório.
|
|
3780
|
+
*/
|
|
3781
|
+
for (const titulo of DECIDEM) {
|
|
3782
|
+
const achada = secoes.find((s) => s.titulo === titulo);
|
|
3783
|
+
if (!achada)
|
|
3784
|
+
continue;
|
|
3785
|
+
console.log(section(titulo));
|
|
3786
|
+
for (const linha of achada.linhas)
|
|
3787
|
+
console.log(linha);
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* O ARQUIVO SÓ NASCE SE ELE PEDIR (dono, 18/09) - e é isto que mantém `INV-INSPECT-01` de pé.
|
|
3791
|
+
*
|
|
3792
|
+
* A promessa que faz alguém rodar este comando sem medo é que nada acontece: ele lê, mostra, e
|
|
3793
|
+
* o repositório continua exatamente como estava. Escrever o relatório por conta própria
|
|
3794
|
+
* quebraria isso para TODO mundo, inclusive para quem só queria olhar - e num primeiro contato
|
|
3795
|
+
* essa é a confiança inteira. Perguntando, a escrita deixa de ser nossa e passa a ser dele.
|
|
3796
|
+
*
|
|
3797
|
+
* SEM TERMINAL, NÃO PERGUNTA E NÃO ESCREVE: um `inspect` dentro de um pipe, de um CI ou de um
|
|
3798
|
+
* agente não tem quem responda, e um default que escreve seria a mesma quebra por outra porta.
|
|
3799
|
+
* É a mesma guarda que `askName` já usa.
|
|
3800
|
+
*/
|
|
3801
|
+
console.log(section("The whole reading, in one file"));
|
|
3802
|
+
console.log(body(`${secoes.length} sections did not print here - every value, every fragment, every part.`));
|
|
3803
|
+
console.log(body(paint.faint("A single self-contained page in your project. Nothing is sent either way.")));
|
|
3804
|
+
if (!(await quer(` write the full report? [Y/n]: `))) {
|
|
3805
|
+
console.log("");
|
|
3806
|
+
console.log(body("Nothing was written, and nothing was sent."));
|
|
3807
|
+
console.log("");
|
|
3808
|
+
console.log(body("To let your agent ask before it invents:"));
|
|
3809
|
+
console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
|
|
3810
|
+
console.log("");
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
await mkdir(dirname(arquivo), { recursive: true });
|
|
3814
|
+
/**
|
|
3815
|
+
* A PASTA SE DECLARA GERENCIADA ANTES DE RECEBER O ARQUIVO: sem isto, o HTML apareceria no
|
|
3816
|
+
* `git status` de quem acabou de pedir um relatório, e não um arquivo versionado.
|
|
3817
|
+
*/
|
|
3818
|
+
await writeGovernanceIgnore(root).catch(() => null);
|
|
3819
|
+
await writeFile(arquivo, html(), "utf8");
|
|
3820
|
+
console.log("");
|
|
3821
|
+
console.log(body(` ${paint.strong(relative(root, arquivo))}`));
|
|
3822
|
+
console.log(body(` ${paint.dim(`file://${arquivo}`)}`));
|
|
3823
|
+
console.log("");
|
|
3824
|
+
console.log(body("To let your agent ask before it invents:"));
|
|
3825
|
+
console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
|
|
3826
|
+
console.log("");
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* A PERGUNTA DE UMA TECLA, e o silêncio de quem não tem terminal.
|
|
3830
|
+
*
|
|
3831
|
+
* Fora de um terminal a resposta é NÃO, sempre: ninguém está lá para responder, e assumir o sim
|
|
3832
|
+
* escreveria no repositório de um CI que só queria ler. Enter é sim porque a pessoa acabou de ler
|
|
3833
|
+
* a oferta na linha acima - exigir uma letra para receber o que a tela ofereceu é atrito sem
|
|
3834
|
+
* proteção nenhuma.
|
|
3835
|
+
*/
|
|
3836
|
+
async function quer(pergunta) {
|
|
3837
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
3838
|
+
return false;
|
|
3839
|
+
const { createInterface } = await import("node:readline/promises");
|
|
3840
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3841
|
+
try {
|
|
3842
|
+
console.log("");
|
|
3843
|
+
const resposta = (await rl.question(pergunta)).trim().toLowerCase();
|
|
3844
|
+
return resposta === "" || resposta === "y" || resposta === "yes";
|
|
3845
|
+
}
|
|
3846
|
+
catch {
|
|
3847
|
+
return false;
|
|
3848
|
+
}
|
|
3849
|
+
finally {
|
|
3850
|
+
rl.close();
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* AS SEÇÕES QUE CONTINUAM NA TELA, e o critério é um só: elas mudam o comando seguinte.
|
|
3855
|
+
*
|
|
3856
|
+
* Um root com três apps mede a média de três paletas, o que é diagnóstico e não sistema - e a saída
|
|
3857
|
+
* já traz o `--scope`/`--usage` exato. Um `var()` que não resolve não pinta cor nenhuma no
|
|
3858
|
+
* navegador dele. As outras quinze seções descrevem; estas duas pedem uma decisão.
|
|
3859
|
+
*/
|
|
3860
|
+
const DECIDEM = [
|
|
3861
|
+
"This root holds several projects",
|
|
3862
|
+
"These resolve to nothing",
|
|
3863
|
+
];
|
|
3723
3864
|
function sayReach(c) {
|
|
3724
3865
|
const reach = ladderReach(c.declared);
|
|
3725
3866
|
console.log("");
|
|
@@ -4332,6 +4473,16 @@ export async function runImport(opts) {
|
|
|
4332
4473
|
if (!opts.inspect)
|
|
4333
4474
|
phase(1, 3, "Measuring the repository");
|
|
4334
4475
|
console.log(section("Reading your project"));
|
|
4476
|
+
/**
|
|
4477
|
+
* A PARTIR DAQUI O `inspect` GUARDA EM VEZ DE IMPRIMIR - ver `transcript.ts`.
|
|
4478
|
+
*
|
|
4479
|
+
* As dezessete seções continuam sendo escritas exatamente como sempre foram; o que muda é o
|
|
4480
|
+
* destino. Elas vão para o relatório, e o terminal fica com os quatro blocos que uma pessoa
|
|
4481
|
+
* usa para DECIDIR. Medido na saída do `codelevel-monorepo` em 18/09: 250 linhas, das quais 94
|
|
4482
|
+
* eram a mesma frase sobre nome de parte - 40% do relatório num detalhe sem ação possível.
|
|
4483
|
+
*/
|
|
4484
|
+
if (opts.inspect)
|
|
4485
|
+
ligarCaptura();
|
|
4335
4486
|
/**
|
|
4336
4487
|
* UMA MEDIÇÃO POR ESCOPO, E DEPOIS A FUSÃO.
|
|
4337
4488
|
*
|
|
@@ -4448,13 +4599,8 @@ export async function runImport(opts) {
|
|
|
4448
4599
|
*/
|
|
4449
4600
|
if (opts.inspect) {
|
|
4450
4601
|
sayReach(census);
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
console.log(body("Nothing was written, and nothing was sent."));
|
|
4454
|
-
console.log("");
|
|
4455
|
-
console.log(body("To let it ask before it invents:"));
|
|
4456
|
-
console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
|
|
4457
|
-
console.log("");
|
|
4602
|
+
const transcrito = desligarCaptura();
|
|
4603
|
+
await mostrarInspecao(census, root, transcrito);
|
|
4458
4604
|
return;
|
|
4459
4605
|
}
|
|
4460
4606
|
const out = opts.census ?? join(root, "_synthesisui", "census.json");
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1790,13 +1790,27 @@ cli) {
|
|
|
1790
1790
|
const token = await readToken();
|
|
1791
1791
|
if (!token)
|
|
1792
1792
|
return fromContract("Not signed in, so nothing could be sent. A person has to run `synthesisui login` once in the terminal - it opens a browser, and I cannot complete it for them.");
|
|
1793
|
-
const { out } = await capturing(() => remeasure({
|
|
1793
|
+
const { out, value: refused } = await capturing(() => remeasure({
|
|
1794
1794
|
root,
|
|
1795
1795
|
base: resolveRegistry(),
|
|
1796
1796
|
token,
|
|
1797
1797
|
slug,
|
|
1798
1798
|
cli: VERSION,
|
|
1799
1799
|
}));
|
|
1800
|
+
/**
|
|
1801
|
+
* O TETO DO PLANO CHEGA AO AGENTE COMO RESPOSTA, e não como silêncio.
|
|
1802
|
+
*
|
|
1803
|
+
* Esta ferramenta é uma ATUALIZAÇÃO, não uma consulta: ela re-mede o repositório e envia.
|
|
1804
|
+
* Então ela conta contra o teto diário do plano grátis, e quando o teto é atingido o envio
|
|
1805
|
+
* não acontece. Sem esta linha o agente receberia a saída capturada sem a recusa dentro
|
|
1806
|
+
* dela - `remeasure` devolve o motivo em vez de imprimi-lo, para o `sync` poder dizê-lo por
|
|
1807
|
+
* último no terminal - e concluiria que a medição subiu.
|
|
1808
|
+
*
|
|
1809
|
+
* A frase vem do servidor inteira, com o endereço junto, porque aqui não há terminal para
|
|
1810
|
+
* quebrar linha: quem lê é o agente, e ele precisa do endereço para repassar à pessoa.
|
|
1811
|
+
*/
|
|
1812
|
+
if (refused)
|
|
1813
|
+
return fromContract(`${refused.message}${refused.url ? `\n\n ${refused.url}` : ""}\n\nNothing was lost: the measurement is on disk and every question you ask about this system keeps being answered. Say this to the person instead of retrying.`);
|
|
1800
1814
|
/**
|
|
1801
1815
|
* E A DEFASAGEM DO HOOK, EM TRÊS LINHAS - não num parágrafo.
|
|
1802
1816
|
*
|
package/dist/commands/sync.js
CHANGED
|
@@ -168,8 +168,20 @@ export async function sync(opts) {
|
|
|
168
168
|
* Então: a plataforma devolve o que já foi decidido (o escopo e a leitura), o CLI mede com o leitor
|
|
169
169
|
* ATUAL, funde a leitura no censo fresco, AVISA o que seria reescrito, e envia. Sem entrevista.
|
|
170
170
|
*/
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
/**
|
|
172
|
+
* A RECUSA É GUARDADA PARA O FIM, e essa é a diferença entre informar e enganar.
|
|
173
|
+
*
|
|
174
|
+
* O comando continua depois da medição: ele fecha pedidos, manda o registro de checagens e
|
|
175
|
+
* termina imprimindo "N checks sent". Uma recusa dita no meio disso rola para cima, e a ÚLTIMA
|
|
176
|
+
* linha da tela anuncia sucesso para uma rodada em que o design system dela não foi atualizado.
|
|
177
|
+
*
|
|
178
|
+
* O registro de checagens SOBE mesmo assim, e isso é deliberado: é outra rota, é o que alimenta
|
|
179
|
+
* "você está na v2 e existe a v4", e não é o envio pesado que a cota governa. Dizer as duas
|
|
180
|
+
* coisas na ordem em que aconteceram é mais honesto que abortar tudo.
|
|
181
|
+
*/
|
|
182
|
+
const refused = opts.recordOnly
|
|
183
|
+
? null
|
|
184
|
+
: await remeasure({
|
|
173
185
|
root,
|
|
174
186
|
base,
|
|
175
187
|
token,
|
|
@@ -206,6 +218,7 @@ export async function sync(opts) {
|
|
|
206
218
|
if (emptyLedger && !repo && forms.length === 0) {
|
|
207
219
|
console.log(section("Sync"));
|
|
208
220
|
console.log(body("Nothing recorded yet. The record fills as the hook checks writes and doctor runs."));
|
|
221
|
+
sayRefused(refused);
|
|
209
222
|
return;
|
|
210
223
|
}
|
|
211
224
|
const res = await fetch(`${base}/api/ledger`, {
|
|
@@ -308,21 +321,29 @@ export async function sync(opts) {
|
|
|
308
321
|
* nada, que é o caso normal.
|
|
309
322
|
*/
|
|
310
323
|
await reportWhatIsLeft(root);
|
|
324
|
+
sayRefused(refused);
|
|
311
325
|
}
|
|
312
326
|
/**
|
|
313
|
-
*
|
|
327
|
+
* O QUE NÃO FOI ENVIADO, DITO POR ÚLTIMO - e o "por último" é o desenho, não um detalhe.
|
|
314
328
|
*
|
|
315
|
-
* A
|
|
329
|
+
* A pessoa lê a última seção da tela e é ela que decide o que ela conclui do comando. O registro
|
|
330
|
+
* de checagens subiu de verdade, então a linha "N checks sent" é verdadeira e fica; o que não
|
|
331
|
+
* pode é ela ser a ÚLTIMA coisa numa rodada em que o design system não foi atualizado.
|
|
316
332
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* 3. funde a leitura gravada no censo fresco. A anatomia é do agente; a medição é nossa.
|
|
322
|
-
* 4. AVISA antes de enviar o que seria reescrito, e pergunta. Avisar depois de escrever não é
|
|
323
|
-
* avisar.
|
|
324
|
-
* 5. envia para a rota que atualiza o RASCUNHO pelo slug - nada publicado se move.
|
|
333
|
+
* NÃO SE LÊ COMO ERRO: a frase vem do servidor, que é quem conhece o teto, a contagem e a hora em
|
|
334
|
+
* que a próxima libera. E o endereço vai em linha própria, indentado, como toda ação que o
|
|
335
|
+
* terminal oferece - prosa quebra em 66 colunas e um endereço no meio de um parágrafo não se
|
|
336
|
+
* distingue do parágrafo.
|
|
325
337
|
*/
|
|
338
|
+
function sayRefused(refused) {
|
|
339
|
+
if (!refused)
|
|
340
|
+
return;
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log(section("Your system was not updated"));
|
|
343
|
+
console.log(body(refused.message));
|
|
344
|
+
if (refused.url)
|
|
345
|
+
console.log(body(` ${paint.blue(refused.url)}`));
|
|
346
|
+
}
|
|
326
347
|
export async function remeasure(args) {
|
|
327
348
|
const { root, base, token, slug } = args;
|
|
328
349
|
const head = await fetch(`${base}/api/registry/ds/${slug}/reading`, {
|
|
@@ -333,7 +354,7 @@ export async function remeasure(args) {
|
|
|
333
354
|
console.log(body(head
|
|
334
355
|
? `The registry answered ${head.status} for this system's reading, so nothing was re-measured. The record below still travels.`
|
|
335
356
|
: "Could not reach the registry, so nothing was re-measured. The record below still travels."));
|
|
336
|
-
return;
|
|
357
|
+
return null;
|
|
337
358
|
}
|
|
338
359
|
const stored = (await head.json());
|
|
339
360
|
/**
|
|
@@ -343,7 +364,7 @@ export async function remeasure(args) {
|
|
|
343
364
|
if (!stored.hasCensus) {
|
|
344
365
|
console.log(section("Sync"));
|
|
345
366
|
console.log(body(`"${slug}" did not come from an import, so there is no measurement to refresh. Import it first: npx synthesisui import`));
|
|
346
|
-
return;
|
|
367
|
+
return null;
|
|
347
368
|
}
|
|
348
369
|
/**
|
|
349
370
|
* A DISTÂNCIA ATÉ O CLI PUBLICADO, dita onde ele já está.
|
|
@@ -461,7 +482,7 @@ export async function remeasure(args) {
|
|
|
461
482
|
: null;
|
|
462
483
|
if (!census) {
|
|
463
484
|
console.log(body("The measurement failed, so nothing was sent."));
|
|
464
|
-
return;
|
|
485
|
+
return null;
|
|
465
486
|
}
|
|
466
487
|
/** A LEITURA GRAVADA ENTRA NO CENSO FRESCO - ver a rota `reading`. */
|
|
467
488
|
if (stored.reading)
|
|
@@ -564,7 +585,7 @@ export async function remeasure(args) {
|
|
|
564
585
|
.join(" · "))));
|
|
565
586
|
if (!(await askToOverwrite())) {
|
|
566
587
|
console.log(body("Nothing was sent. Your draft is untouched."));
|
|
567
|
-
return;
|
|
588
|
+
return null;
|
|
568
589
|
}
|
|
569
590
|
}
|
|
570
591
|
/**
|
|
@@ -634,11 +655,33 @@ export async function remeasure(args) {
|
|
|
634
655
|
: {}),
|
|
635
656
|
}),
|
|
636
657
|
}).catch(() => null);
|
|
658
|
+
/**
|
|
659
|
+
* O TETO DE ATUALIZAÇÕES DO PLANO GRÁTIS - e ele NÃO se lê como um erro, porque não é um.
|
|
660
|
+
*
|
|
661
|
+
* O QUE ELA VIVE: o envio parou porque o plano dela atualiza um número de vezes por dia, e a
|
|
662
|
+
* frase diz quantas já foram, que a medição continua no disco dela, que as perguntas do agente
|
|
663
|
+
* seguem respondendo, e a HORA em que a próxima libera. O endereço do plano vai em linha
|
|
664
|
+
* própria, indentado, como toda ação que o terminal oferece - prosa quebra em 66 colunas e um
|
|
665
|
+
* endereço enterrado no meio de um parágrafo não se distingue do parágrafo.
|
|
666
|
+
*
|
|
667
|
+
* A FRASE VEM DO SERVIDOR e não é escrita aqui: é ele quem conhece o teto, a contagem e o
|
|
668
|
+
* horário. Uma segunda redação deste texto no CLI é o dia em que alguém muda a cota e o
|
|
669
|
+
* terminal continua anunciando a antiga - e o terminal é onde a pessoa está parada.
|
|
670
|
+
*/
|
|
671
|
+
if (res?.status === 429) {
|
|
672
|
+
const door = (await res.json().catch(() => null));
|
|
673
|
+
return {
|
|
674
|
+
kind: "quota",
|
|
675
|
+
message: door?.message ??
|
|
676
|
+
"Your free plan has reached its updates for today, so your design system was not updated.",
|
|
677
|
+
url: door?.url ?? null,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
637
680
|
if (!res?.ok) {
|
|
638
681
|
console.log(body(res
|
|
639
682
|
? `The registry answered ${res.status}. Nothing was written.`
|
|
640
683
|
: "Could not reach the registry. Nothing was written."));
|
|
641
|
-
return;
|
|
684
|
+
return null;
|
|
642
685
|
}
|
|
643
686
|
const out = (await res.json());
|
|
644
687
|
console.log("");
|
|
@@ -842,6 +885,8 @@ export async function remeasure(args) {
|
|
|
842
885
|
console.log(body(paint.faint(`${work.measured} declared piece${work.measured === 1 ? "" : "s"} of work re-checked against this measurement`)));
|
|
843
886
|
if (!args.full)
|
|
844
887
|
console.log(body(paint.faint("everything the reader saw: npx synthesisui sync --full")));
|
|
888
|
+
/** Chegou até aqui: a medição foi enviada e escrita. Nada a recusar. */
|
|
889
|
+
return null;
|
|
845
890
|
}
|
|
846
891
|
/**
|
|
847
892
|
* Pergunta uma vez, e só num terminal de verdade.
|
package/dist/install-marks.js
CHANGED
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
* chama `wireAgent`, então rodar o comando é o caminho de volta - e ele só alarga a string que era
|
|
236
236
|
* nossa, nunca um filtro que uma pessoa escreveu.
|
|
237
237
|
*/
|
|
238
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
238
|
+
export const MATERIALISER_SINCE = "0.16.429";
|
|
239
239
|
/**
|
|
240
240
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
241
241
|
*
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { semCor } from "./transcript.js";
|
|
2
|
+
/**
|
|
3
|
+
* O QUE O TERMINAL DIRIA, LIDO DE VOLTA COMO DOCUMENTO.
|
|
4
|
+
*
|
|
5
|
+
* A régua `── Título ──────` é a única marca estrutural que a saída tem, e ela é desenhada num
|
|
6
|
+
* lugar só (`section`), então é ela que separa os capítulos. Tudo antes da primeira régua é ruído
|
|
7
|
+
* de progresso e não entra: o documento começa no primeiro capítulo, não no meio do carregamento.
|
|
8
|
+
*/
|
|
9
|
+
export function secoesDe(linhas) {
|
|
10
|
+
const secoes = [];
|
|
11
|
+
for (const bruta of linhas) {
|
|
12
|
+
for (const linha of semCor(bruta).split("\n")) {
|
|
13
|
+
const cabeca = /^──\s(.+?)\s─*$/.exec(linha.trimEnd());
|
|
14
|
+
if (cabeca) {
|
|
15
|
+
secoes.push({ titulo: cabeca[1].trim(), linhas: [] });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (secoes.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
secoes[secoes.length - 1].linhas.push(linha.replace(/\s+$/, ""));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Capítulo que não disse nada não vira capítulo - uma régua sozinha não é conteúdo. */
|
|
24
|
+
return secoes
|
|
25
|
+
.map((s) => ({ ...s, linhas: aparar(s.linhas) }))
|
|
26
|
+
.filter((s) => s.linhas.length > 0);
|
|
27
|
+
}
|
|
28
|
+
/** Tira as linhas em branco das pontas, e nunca as do meio - lá elas são respiro. */
|
|
29
|
+
function aparar(linhas) {
|
|
30
|
+
let i = 0;
|
|
31
|
+
let f = linhas.length;
|
|
32
|
+
while (i < f && linhas[i].trim() === "")
|
|
33
|
+
i += 1;
|
|
34
|
+
while (f > i && linhas[f - 1].trim() === "")
|
|
35
|
+
f -= 1;
|
|
36
|
+
return linhas.slice(i, f);
|
|
37
|
+
}
|
|
38
|
+
function escapar(s) {
|
|
39
|
+
return s
|
|
40
|
+
.replace(/&/g, "&")
|
|
41
|
+
.replace(/</g, "<")
|
|
42
|
+
.replace(/>/g, ">")
|
|
43
|
+
.replace(/"/g, """);
|
|
44
|
+
}
|
|
45
|
+
export function relatorioHtml(input) {
|
|
46
|
+
const { cabecalho, destaques, secoes } = input;
|
|
47
|
+
const quando = legivel(cabecalho.medidoEm);
|
|
48
|
+
const indice = secoes
|
|
49
|
+
.map((s, i) => `<li><a href="#s${i}">${escapar(s.titulo)}</a></li>`)
|
|
50
|
+
.join("\n ");
|
|
51
|
+
const corpo = secoes
|
|
52
|
+
.map((s, i) => ` <section id="s${i}">
|
|
53
|
+
<h2>${escapar(s.titulo)}</h2>
|
|
54
|
+
<pre>${escapar(s.linhas.join("\n"))}</pre>
|
|
55
|
+
</section>`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
const numeros = destaques
|
|
58
|
+
.map((d) => ` <div class="stat">
|
|
59
|
+
<strong>${escapar(d.numero)}</strong>
|
|
60
|
+
<span>${escapar(d.rotulo)}</span>
|
|
61
|
+
</div>`)
|
|
62
|
+
.join("\n");
|
|
63
|
+
return `<!doctype html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8">
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
68
|
+
<title>${escapar(cabecalho.repo)} - read by SynthesisUI</title>
|
|
69
|
+
<style>
|
|
70
|
+
${estilo()}
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<header>
|
|
75
|
+
<p class="kicker">Read by SynthesisUI</p>
|
|
76
|
+
<h1>${escapar(cabecalho.repo)}</h1>
|
|
77
|
+
<p class="meta">Read ${escapar(quando)} by CLI ${escapar(cabecalho.cli)} · nothing left this machine</p>
|
|
78
|
+
</header>
|
|
79
|
+
|
|
80
|
+
<div class="wrap">
|
|
81
|
+
<div class="stats">
|
|
82
|
+
${numeros}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<nav>
|
|
86
|
+
<p class="kicker">What is in here</p>
|
|
87
|
+
<ol>
|
|
88
|
+
${indice}
|
|
89
|
+
</ol>
|
|
90
|
+
</nav>
|
|
91
|
+
|
|
92
|
+
<main>
|
|
93
|
+
${corpo}
|
|
94
|
+
</main>
|
|
95
|
+
|
|
96
|
+
<footer>
|
|
97
|
+
<p>This file lives in your project and was never sent anywhere. Re-run the command to refresh it.</p>
|
|
98
|
+
</footer>
|
|
99
|
+
</div>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
/** A data em palavras. Data ilegível some da frase em vez de imprimir `Invalid Date`. */
|
|
105
|
+
function legivel(iso) {
|
|
106
|
+
if (!iso || Number.isNaN(Date.parse(iso)))
|
|
107
|
+
return "just now";
|
|
108
|
+
return new Date(iso).toLocaleDateString("en-US", {
|
|
109
|
+
month: "short",
|
|
110
|
+
day: "numeric",
|
|
111
|
+
year: "numeric",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A FOLHA, E ELA É INTEIRA DAQUI: nenhuma família remota, nenhum reset de terceiro. As famílias são
|
|
116
|
+
* as genéricas do sistema de quem abrir, porque baixar uma fonte para desenhar um relatório sobre o
|
|
117
|
+
* repositório de alguém contradiz o comando que não manda nada para lugar nenhum.
|
|
118
|
+
*
|
|
119
|
+
* E ela imprime: `@media print` existe porque o navegador já exporta PDF, e um relatório que sai
|
|
120
|
+
* torto no papel obriga a construir um gerador de PDF para resolver o que uma regra resolve.
|
|
121
|
+
*/
|
|
122
|
+
function estilo() {
|
|
123
|
+
return `:root {
|
|
124
|
+
color-scheme: light dark;
|
|
125
|
+
--ink: #16161a;
|
|
126
|
+
--ink-soft: #55555f;
|
|
127
|
+
--line: #e4e4e9;
|
|
128
|
+
--paper: #ffffff;
|
|
129
|
+
--canvas: #f6f6f8;
|
|
130
|
+
--accent: #b4530a;
|
|
131
|
+
}
|
|
132
|
+
@media (prefers-color-scheme: dark) {
|
|
133
|
+
:root {
|
|
134
|
+
--ink: #edecea;
|
|
135
|
+
--ink-soft: #a0a0aa;
|
|
136
|
+
--line: #2a2a31;
|
|
137
|
+
--paper: #17171b;
|
|
138
|
+
--canvas: #0f0f12;
|
|
139
|
+
--accent: #e9a13b;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
* { box-sizing: border-box; }
|
|
143
|
+
body {
|
|
144
|
+
margin: 0;
|
|
145
|
+
background: var(--canvas);
|
|
146
|
+
color: var(--ink);
|
|
147
|
+
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
148
|
+
}
|
|
149
|
+
header {
|
|
150
|
+
padding: 56px 24px 40px;
|
|
151
|
+
border-bottom: 1px solid var(--line);
|
|
152
|
+
background: var(--paper);
|
|
153
|
+
}
|
|
154
|
+
.wrap, header > * { max-width: 900px; margin-inline: auto; }
|
|
155
|
+
.wrap { padding: 0 24px 72px; }
|
|
156
|
+
.kicker {
|
|
157
|
+
margin: 0;
|
|
158
|
+
font: 600 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
159
|
+
letter-spacing: 0.16em;
|
|
160
|
+
text-transform: uppercase;
|
|
161
|
+
color: var(--accent);
|
|
162
|
+
}
|
|
163
|
+
h1 { margin: 12px 0 8px; font-size: 34px; line-height: 1.15; letter-spacing: -0.02em; }
|
|
164
|
+
.meta { margin: 0; color: var(--ink-soft); font-size: 14px; }
|
|
165
|
+
.stats {
|
|
166
|
+
display: grid;
|
|
167
|
+
gap: 12px;
|
|
168
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
169
|
+
margin: -28px 0 40px;
|
|
170
|
+
}
|
|
171
|
+
.stat {
|
|
172
|
+
background: var(--paper);
|
|
173
|
+
border: 1px solid var(--line);
|
|
174
|
+
border-radius: 14px;
|
|
175
|
+
padding: 18px 20px;
|
|
176
|
+
}
|
|
177
|
+
.stat strong { display: block; font-size: 26px; letter-spacing: -0.02em; }
|
|
178
|
+
.stat span { display: block; margin-top: 4px; color: var(--ink-soft); font-size: 13px; }
|
|
179
|
+
nav { margin-bottom: 40px; }
|
|
180
|
+
nav ol { margin: 10px 0 0; padding-left: 20px; color: var(--ink-soft); }
|
|
181
|
+
nav li { margin: 3px 0; }
|
|
182
|
+
nav a { color: inherit; }
|
|
183
|
+
nav a:hover { color: var(--accent); }
|
|
184
|
+
section {
|
|
185
|
+
background: var(--paper);
|
|
186
|
+
border: 1px solid var(--line);
|
|
187
|
+
border-radius: 14px;
|
|
188
|
+
padding: 22px 24px;
|
|
189
|
+
margin-bottom: 16px;
|
|
190
|
+
}
|
|
191
|
+
h2 { margin: 0 0 12px; font-size: 17px; letter-spacing: -0.01em; }
|
|
192
|
+
pre {
|
|
193
|
+
margin: 0;
|
|
194
|
+
white-space: pre-wrap;
|
|
195
|
+
word-break: break-word;
|
|
196
|
+
font: 12.5px/1.65 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
197
|
+
color: var(--ink-soft);
|
|
198
|
+
}
|
|
199
|
+
footer { margin-top: 40px; color: var(--ink-soft); font-size: 13px; }
|
|
200
|
+
@media print {
|
|
201
|
+
body { background: #fff; }
|
|
202
|
+
.stat, section { break-inside: avoid; border-color: #ddd; }
|
|
203
|
+
nav { display: none; }
|
|
204
|
+
}`;
|
|
205
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE O TERMINAL TERIA IMPRIMIDO, GUARDADO EM VEZ DE MOSTRADO.
|
|
3
|
+
*
|
|
4
|
+
* O QUE O CLIENTE GANHA: a saída do `inspect` volta a caber numa tela, e nada do que ela dizia se
|
|
5
|
+
* perde - o texto inteiro vai para o relatório que ele abre quando quiser.
|
|
6
|
+
*
|
|
7
|
+
* POR QUE AQUI E NÃO EM CADA IMPRESSOR: são dezessete seções escritas em dezessete lugares, e a
|
|
8
|
+
* leitura delas é a mesma coisa que a esteira já sabe dizer. Reescrevê-las num gerador de HTML
|
|
9
|
+
* criaria duas redações da mesma medição - e no dia em que uma fosse editada, o relatório passaria
|
|
10
|
+
* a afirmar algo que o terminal não afirma mais. Capturando, existe UMA fonte: o que a esteira diz.
|
|
11
|
+
*
|
|
12
|
+
* ELE É DELIBERADAMENTE BURRO: liga, guarda, desliga. Sem filtro, sem formatação e sem saber o que
|
|
13
|
+
* é seção - quem entende do assunto é quem transforma isto em documento.
|
|
14
|
+
*/
|
|
15
|
+
let guardadas = null;
|
|
16
|
+
let originalLog = null;
|
|
17
|
+
/**
|
|
18
|
+
* A REDE DE SEGURANÇA, e ela existe por um motivo específico: uma medição que lança no meio
|
|
19
|
+
* deixaria o `console.log` desviado pelo resto do processo, e a pessoa veria o comando morrer sem
|
|
20
|
+
* uma linha de explicação. O terminal volta ao normal mesmo quando ninguém se lembra de desligar.
|
|
21
|
+
*/
|
|
22
|
+
process.once("exit", () => {
|
|
23
|
+
if (originalLog)
|
|
24
|
+
console.log = originalLog;
|
|
25
|
+
});
|
|
26
|
+
/** Começa a guardar em vez de imprimir. Chamar duas vezes não empilha - a segunda é um no-op. */
|
|
27
|
+
export function ligarCaptura() {
|
|
28
|
+
if (guardadas)
|
|
29
|
+
return;
|
|
30
|
+
guardadas = [];
|
|
31
|
+
originalLog = console.log;
|
|
32
|
+
console.log = (...args) => {
|
|
33
|
+
guardadas?.push(args.map((a) => (typeof a === "string" ? a : String(a))).join(" "));
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Devolve o terminal a quem é dele, e entrega o que foi guardado. */
|
|
37
|
+
export function desligarCaptura() {
|
|
38
|
+
const linhas = guardadas ?? [];
|
|
39
|
+
if (originalLog)
|
|
40
|
+
console.log = originalLog;
|
|
41
|
+
guardadas = null;
|
|
42
|
+
originalLog = null;
|
|
43
|
+
return linhas;
|
|
44
|
+
}
|
|
45
|
+
/** O caractere de escape ANSI, montado por código para não viajar cru no arquivo. */
|
|
46
|
+
const ESC = String.fromCharCode(27);
|
|
47
|
+
/** Tira a cor: o que vai para um arquivo não carrega sequência de escape de terminal. */
|
|
48
|
+
export function semCor(line) {
|
|
49
|
+
return line.split(new RegExp(`${ESC}\\[[0-9;]*m`)).join("");
|
|
50
|
+
}
|
package/package.json
CHANGED