synthesisui 0.16.411 → 0.16.413
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/commands/component.js +111 -6
- package/dist/commands/connect.js +20 -0
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/generate.js +18 -1
- package/dist/commands/hook.js +57 -4
- package/dist/commands/refit.js +21 -1
- package/dist/commands/sync.js +18 -0
- package/dist/commands/template.js +159 -24
- package/dist/commands/upgrade.js +64 -8
- package/dist/doctor/ci-format.js +7 -5
- package/dist/index.js +28 -1
- package/dist/install-marks.js +49 -2
- package/dist/is-a-project.js +78 -0
- package/dist/sheet-needed.js +89 -2
- package/dist/their-tongue.js +27 -0
- package/dist/wiring-read.js +22 -0
- package/dist/written.js +27 -5
- package/package.json +1 -1
|
@@ -8,10 +8,11 @@ import { detectAppDirs } from "../global-sheet.js";
|
|
|
8
8
|
import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
|
|
9
9
|
import { body, section, snippet } from "../output.js";
|
|
10
10
|
import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
|
|
11
|
-
import { fetchComponent, RegistryError } from "../registry.js";
|
|
11
|
+
import { fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
12
12
|
import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
|
|
13
13
|
import { flavourResolver } from "../styles-flavour.js";
|
|
14
|
-
import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
|
|
14
|
+
import { inTheirTongue, projectTongue, sumSpoken, tongueFromArtifacts, } from "../their-tongue.js";
|
|
15
|
+
import { resolvableVars } from "../their-vars.js";
|
|
15
16
|
import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
|
|
16
17
|
import { readWiring } from "../wiring-read.js";
|
|
17
18
|
import { editedSinceWritten, readWritten, recordWritten } from "../written.js";
|
|
@@ -114,7 +115,38 @@ export async function component(slug, name, opts) {
|
|
|
114
115
|
* apagaria a cor. Um projeto de destino chega sem mapa no `.lock`, nada é traduzido, e a folha
|
|
115
116
|
* continua sendo o caminho - o comando DIZ qual dos dois aconteceu.
|
|
116
117
|
*/
|
|
117
|
-
const
|
|
118
|
+
const installedTongue = await projectTongue(root, slug);
|
|
119
|
+
/**
|
|
120
|
+
* E SEM O `add`, O SISTEMA VEM DO REGISTRY E O VOCABULARIO E' DERIVADO EM MEMORIA.
|
|
121
|
+
*
|
|
122
|
+
* O QUE ACONTECIA (medido em 10/09): sem `.lock`, `projectTongue` responde `null`, nada era
|
|
123
|
+
* traduzido, e o arquivo saia com `var(--ds-*)` cru - inutil ate' que a pessoa rodasse `add` e
|
|
124
|
+
* materializasse 1813 linhas, 111 variaveis e 628 referencias no repositorio dela. Pedir UM
|
|
125
|
+
* botao passava por instalar o sistema inteiro.
|
|
126
|
+
*
|
|
127
|
+
* A NOSSA FOLHA E' A REFERENCIA, NAO O PRE-REQUISITO (a definicao do produto, 07/09): ela existe
|
|
128
|
+
* para o agente dele ler e construir sobre ela. A medicao que ela carregava - quais das nossas
|
|
129
|
+
* variaveis tem nome no codigo dele - nao precisa de disco: e' a mesma varredura que o `add`
|
|
130
|
+
* faz, feita aqui e jogada fora depois de traduzir.
|
|
131
|
+
*
|
|
132
|
+
* UMA IDA A MAIS A' REDE, e so' neste caminho: quem ja' instalou o sistema le' do `.lock` como
|
|
133
|
+
* sempre. O `catch` mantem o comportamento antigo - sem tradução, e o bloco de setup abaixo diz
|
|
134
|
+
* que a folha e' o caminho.
|
|
135
|
+
*/
|
|
136
|
+
const fromRegistry = installedTongue
|
|
137
|
+
? null
|
|
138
|
+
: await fetchDesignSystem(base, slug, opts.version).catch(() => null);
|
|
139
|
+
/** A leitura do sistema NAO chegou - rede fora, ou sistema privado sem sessao. Ver o relato. */
|
|
140
|
+
const installedSheetMissing = !installedTongue && fromRegistry === null;
|
|
141
|
+
/**
|
|
142
|
+
* A NOSSA FOLHA ESTA' NA PASTA DELE? - o `.lock` responde, e e' o mesmo arquivo que o `add`
|
|
143
|
+
* escreve. Desde o A1 esta pergunta deixou de ser retorica: `component` roda sem ele.
|
|
144
|
+
*/
|
|
145
|
+
const installedHere = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8").then(() => true, () => false);
|
|
146
|
+
const tongue = installedTongue ??
|
|
147
|
+
(fromRegistry
|
|
148
|
+
? await tongueFromArtifacts(root, fromRegistry.artifacts)
|
|
149
|
+
: null);
|
|
118
150
|
/**
|
|
119
151
|
* O RELATÓRIO SÓ EXISTE DEPOIS DE OS BYTES EXISTIREM - e é essa ordem que corrige o defeito.
|
|
120
152
|
*
|
|
@@ -422,6 +454,45 @@ export async function component(slug, name, opts) {
|
|
|
422
454
|
console.log(spoken.left.length === 0
|
|
423
455
|
? ` No variable in what was just written points at our stylesheet - they are all names YOUR code declares.`
|
|
424
456
|
: ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css carries ${spoken.left.length === 1 ? "it" : "those"}.`);
|
|
457
|
+
/**
|
|
458
|
+
* E DIZER DE ONDE VEIO O VOCABULARIO, quando o sistema NAO esta' instalado aqui.
|
|
459
|
+
*
|
|
460
|
+
* Sem esta linha o resultado seria magico: o arquivo fala a lingua dele e nada explica como.
|
|
461
|
+
* A frase importa porque a conclusao natural - *"entao o `add` ja' rodou"* - esta' errada, e a
|
|
462
|
+
* pessoa precisa saber que o que ela tem e' o componente, e que a folha continua sendo uma
|
|
463
|
+
* escolha e nao uma pendencia.
|
|
464
|
+
*/
|
|
465
|
+
if (fromRegistry)
|
|
466
|
+
console.log(` Nothing of ours was installed to do that - the system was read at v${fromRegistry.version}, and the names came from your own code.`);
|
|
467
|
+
}
|
|
468
|
+
else if (!tongue) {
|
|
469
|
+
/**
|
|
470
|
+
* A LACUNA DECLARADA (lei 8): nada foi traduzido, e ISSO SE DIZ.
|
|
471
|
+
*
|
|
472
|
+
* O QUE ACONTECIA SEM ESTA LINHA: o `.tsx` saia com `var(--ds-*)` cru e o terminal ficava
|
|
473
|
+
* mudo. A pessoa abre o arquivo, ve' variaveis que o `globals.css` dela nao declara, e a
|
|
474
|
+
* unica forma de descobrir por que a cor nao apareceu e' ir ler o nosso codigo.
|
|
475
|
+
*
|
|
476
|
+
* E OS DOIS MOTIVOS SAO DIFERENTES, entao a frase distingue: o repositorio dela nao nomeia
|
|
477
|
+
* nenhum destes valores (o caso comum, e nao ha' o que fazer), ou a leitura do sistema nao
|
|
478
|
+
* chegou - rede fora, sistema privado sem sessao. O segundo tem conserto e o primeiro nao.
|
|
479
|
+
*/
|
|
480
|
+
/**
|
|
481
|
+
* E AS TRES CAUSAS SAO DIFERENTES, entao a frase distingue - so' uma delas tem conserto na
|
|
482
|
+
* mao dele, e ela e' a mais provavel das tres.
|
|
483
|
+
*
|
|
484
|
+
* a leitura nao chegou rede fora, sessao ausente, sistema privado
|
|
485
|
+
* o build dele nao existe `resolvableVars` recusa todo par que o build nao emite, por
|
|
486
|
+
* desenho: um nome afirmado sem prova pinta a cor errada no dia
|
|
487
|
+
* em que ele o renomear. Num clone fresco isso e' o caso normal
|
|
488
|
+
* o codigo dele nao nomeia nada a unica que o silencio de fato descrevia
|
|
489
|
+
*/
|
|
490
|
+
const noBuild = !installedSheetMissing && (await resolvableVars(root)) === null;
|
|
491
|
+
console.log(installedSheetMissing
|
|
492
|
+
? ` Nothing was translated: "${slug}" could not be read from the registry just now, so the variables stayed as ours. Check your connection, or run: npx synthesisui login`
|
|
493
|
+
: noBuild
|
|
494
|
+
? ` Nothing was translated: this project has no build output to read, and a name we cannot see your build emit is a name we will not write. Run your build once and ask for it again - the file then speaks your own names.`
|
|
495
|
+
: ` Nothing was translated: your code names none of the values this system declares yet, so the variables stayed as ours and tokens.css is what resolves them.`);
|
|
425
496
|
}
|
|
426
497
|
// ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
|
|
427
498
|
const tailwind = flavour === "tailwind";
|
|
@@ -454,9 +525,28 @@ export async function component(slug, name, opts) {
|
|
|
454
525
|
*/
|
|
455
526
|
const need = whatOnlyTheSheetResolves({
|
|
456
527
|
source: writtenSource,
|
|
457
|
-
|
|
528
|
+
/**
|
|
529
|
+
* O `@theme` DO SISTEMA - do disco quando ele instalou, do registry quando nao.
|
|
530
|
+
*
|
|
531
|
+
* A pergunta que este bloco faz e' *"sobrou classe que so' o nosso @theme gera?"*. Sem o
|
|
532
|
+
* `add`, `installedThemeCss` responde vazio, e vazio leria como "nao sobrou nada" - uma
|
|
533
|
+
* resposta certa por acidente que ficaria errada no dia em que sobrasse.
|
|
534
|
+
*/
|
|
535
|
+
themeCss: fromRegistry?.artifacts?.["theme.css"] ??
|
|
536
|
+
(await installedThemeCss(root, slug)),
|
|
458
537
|
/** O vocabulário DELE sai da conta - ver `theirNames`. O mapa do `.lock` é a via mais barata. */
|
|
459
538
|
theirNames: tongue?.names ? [...tongue.names.values()] : [],
|
|
539
|
+
/**
|
|
540
|
+
* A TERCEIRA FONTE NÃO É MEDIDA AQUI, e o vazio é a resposta EXPLÍCITA disso.
|
|
541
|
+
*
|
|
542
|
+
* As classes de receita que a folha declara entrariam nesta conta se esta linha entregasse o
|
|
543
|
+
* `tokens.css` instalado - e o efeito está medido em `steps/11-fora-do-escopo.md`: este comando
|
|
544
|
+
* diz *"This file needs nothing else… It is yours."* sobre um componente cujo estilo inteiro vem
|
|
545
|
+
* de `.ds-button`. Ligar a medição aqui é mudar o que este comando promete, com o bloco de setup
|
|
546
|
+
* dele para redesenhar: é etapa própria. O que o argumento obrigatório garante é que a omissão
|
|
547
|
+
* está ESCRITA, e não escondida num parâmetro que ninguém passou.
|
|
548
|
+
*/
|
|
549
|
+
sheetCss: "",
|
|
460
550
|
});
|
|
461
551
|
if (!need.needed) {
|
|
462
552
|
console.log(section("This file needs nothing else"));
|
|
@@ -517,6 +607,19 @@ export async function component(slug, name, opts) {
|
|
|
517
607
|
}
|
|
518
608
|
if (need.needed && !alreadyWired) {
|
|
519
609
|
console.log(section(`One-time setup (once per app, for "${slug}")`));
|
|
610
|
+
/**
|
|
611
|
+
* O PASSO ZERO VEM PRIMEIRO QUANDO A FOLHA NAO EXISTE - e desde o A1 este e' o caso comum.
|
|
612
|
+
*
|
|
613
|
+
* O QUE ACONTECIA: o passo 1 mandava colar `@import ".../tokens.css"` e o aviso de que o
|
|
614
|
+
* sistema nao esta' instalado saia na ULTIMA linha, entre parenteses. Quem nunca rodou `add`
|
|
615
|
+
* - agora um caminho normal, porque o `component` deixou de exigi-lo - lia a instrucao de
|
|
616
|
+
* cima para baixo e importava um caminho que nao existe. O erro do build nao fala de
|
|
617
|
+
* install: fala de um arquivo ausente.
|
|
618
|
+
*
|
|
619
|
+
* O motivo antes da instrucao, e nao depois dela.
|
|
620
|
+
*/
|
|
621
|
+
if (!installedHere)
|
|
622
|
+
console.log(body(`First: "${slug}" is not installed in this project yet, so the file the import below points at does not exist. Run \`npx synthesisui add ${slug}\` before pasting it.`));
|
|
520
623
|
console.log(body(need.variables.length > 0
|
|
521
624
|
? `(what still needs the sheet here: ${need.variables.slice(0, 3).join(", ")}${need.variables.length > 3 ? `, +${need.variables.length - 3}` : ""} - your code names no value for ${need.variables.length === 1 ? "it" : "them"})`
|
|
522
625
|
: `(what still needs the sheet here: the ${need.classes.slice(0, 3).join(", ")}${need.classes.length > 3 ? `, +${need.classes.length - 3}` : ""} ${need.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates)`));
|
|
@@ -529,8 +632,10 @@ export async function component(slug, name, opts) {
|
|
|
529
632
|
console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
|
|
530
633
|
console.log("");
|
|
531
634
|
console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
|
|
532
|
-
|
|
533
|
-
|
|
635
|
+
if (installedHere) {
|
|
636
|
+
console.log("");
|
|
637
|
+
console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
|
|
638
|
+
}
|
|
534
639
|
}
|
|
535
640
|
/**
|
|
536
641
|
* O ELEMENTO QUE SAIU NÃO ATIVA O QUE A RECEITA PEDE - e isso se diz, em vez de sumir (lei 8).
|
package/dist/commands/connect.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
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 { notAProject, projectRootFrom } from "../is-a-project.js";
|
|
6
7
|
import { fmt, say } from "../lang.js";
|
|
7
8
|
import { body, bodyWrapped, paint, section, snippet } from "../output.js";
|
|
8
9
|
import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
|
|
@@ -212,6 +213,25 @@ version) {
|
|
|
212
213
|
}
|
|
213
214
|
export async function connect(opts) {
|
|
214
215
|
const root = opts.dir ?? process.cwd();
|
|
216
|
+
/**
|
|
217
|
+
* ANTES DA PRIMEIRA ESCRITA, SEMPRE - ver `is-a-project.ts`.
|
|
218
|
+
*
|
|
219
|
+
* MEDIDO EM 10/09: este comando tinha 6 escritas e 0 checagens de projeto. Rodado por engano
|
|
220
|
+
* na home ou numa pasta recem-criada, ele deixava `CLAUDE.md`, `.claude/settings.json`,
|
|
221
|
+
* `.mcp.json` e a casa da governanca ali - e um `CLAUDE.md` orfao instrui todo agente que
|
|
222
|
+
* abrir naquela pasta, semanas depois, sem ninguem lembrar de onde ele veio.
|
|
223
|
+
*
|
|
224
|
+
* AQUI E NAO DENTRO DE CADA ESCRITA: recusar na porta e' a unica forma de a promessa
|
|
225
|
+
* *"nada foi escrito"* ser verdade. Meia recusa deixaria dois arquivos e nenhuma mensagem.
|
|
226
|
+
*/
|
|
227
|
+
const project = await projectRootFrom(root);
|
|
228
|
+
if (!project) {
|
|
229
|
+
console.log(section("Not a project"));
|
|
230
|
+
for (const line of notAProject("connect", root))
|
|
231
|
+
console.log(body(line));
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
215
235
|
// Both unless one is explicitly turned off - somebody who says `--no-hook`
|
|
216
236
|
// means it, and somebody who says nothing wants the thing to work.
|
|
217
237
|
const want = { hook: opts.hook !== false, mcp: opts.mcp !== false };
|
package/dist/commands/doctor.js
CHANGED
|
@@ -916,6 +916,13 @@ export async function doctor(opts) {
|
|
|
916
916
|
source: scannedSource,
|
|
917
917
|
themeCss,
|
|
918
918
|
theirNames: installed.theirs.byName.keys(),
|
|
919
|
+
/**
|
|
920
|
+
* A TERCEIRA FONTE NÃO É MEDIDA AQUI - ver o mesmo comentário em `component.ts` e o número em
|
|
921
|
+
* `steps/11-fora-do-escopo.md`. Este comando decide `unwired` e `blocked` por `needed`, então
|
|
922
|
+
* ligar a medição muda o veredito do `doctor` num repositório real: é etapa própria, e o vazio
|
|
923
|
+
* deixa a omissão escrita em vez de escondida.
|
|
924
|
+
*/
|
|
925
|
+
sheetCss: "",
|
|
919
926
|
});
|
|
920
927
|
/**
|
|
921
928
|
* BLOQUEADO SÓ QUANDO A FOLHA É NECESSÁRIA - a mesma pergunta que decide `unwired`.
|
|
@@ -7,7 +7,7 @@ import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalle
|
|
|
7
7
|
import { postGenerate, RegistryError } from "../registry.js";
|
|
8
8
|
import { flavourResolver } from "../styles-flavour.js";
|
|
9
9
|
import { projectTongue } from "../their-tongue.js";
|
|
10
|
-
import { recordWritten } from "../written.js";
|
|
10
|
+
import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
|
|
11
11
|
/** PascalCase para o hint de import (course-card → CourseCard). */
|
|
12
12
|
function pascalName(name) {
|
|
13
13
|
return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
|
|
@@ -76,6 +76,23 @@ export async function generate(description, opts) {
|
|
|
76
76
|
await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
|
|
77
77
|
/** O VOCABULÁRIO DELE - um componente gerado cai no mesmo projeto e fala a mesma língua. */
|
|
78
78
|
await projectTongue(root, slug), await installedThemeVars(root, slug));
|
|
79
|
+
/**
|
|
80
|
+
* A EDICAO DELE VENCE A REESCRITA (T7) - a leitura que faltava.
|
|
81
|
+
*
|
|
82
|
+
* MEDIDO EM 10/09: este comando GRAVAVA o fingerprint e nunca o lia. A promessa existia pela
|
|
83
|
+
* metade - o `upgrade` respeitava o arquivo dele, e o comando que o escreveu na primeira vez
|
|
84
|
+
* passava por cima na segunda. Um pedido parecido devolve o mesmo nome, e o ajuste dele some.
|
|
85
|
+
*
|
|
86
|
+
* O QUE ELE NAO PERDE AO SER RECUSADO: a receita e o CSS ja' estao no disco - so' o `.tsx`
|
|
87
|
+
* nao foi escrito. E a saida e' a mesma que o `component` oferece.
|
|
88
|
+
*/
|
|
89
|
+
const edited = await theirEdits(join(root, "_synthesisui", "ds", slug), res.name, compDir);
|
|
90
|
+
if (edited && edited.length > 0 && !opts.force) {
|
|
91
|
+
console.log("");
|
|
92
|
+
console.log(keptLine(res.name, edited, `npx synthesisui generate "${description}" --name ${res.name} --force`));
|
|
93
|
+
console.log(` The recipe and the compiled CSS are on disk either way - only the .tsx was not written.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
79
96
|
for (const file of files) {
|
|
80
97
|
await writeFile(join(compDir, file.filename), file.code, "utf8");
|
|
81
98
|
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -57,6 +57,20 @@ async function report(root, filePath) {
|
|
|
57
57
|
const rel = relative(root, filePath);
|
|
58
58
|
const d = diagnose([scanSource(rel, src, table)]);
|
|
59
59
|
const named = d.findings.filter((f) => nameToWrite(f));
|
|
60
|
+
/**
|
|
61
|
+
* O VALOR QUE NADA NOMEIA - dito, e nunca batizado (A1 da etapa 11, escolha dele em 10/09).
|
|
62
|
+
*
|
|
63
|
+
* ERA SILÊNCIO, e o silêncio era a única resposta errada disponível. O comentário que estava aqui
|
|
64
|
+
* dizia que a deriva sem nome "não vale interromper, porque o único conselho honesto é pergunte a
|
|
65
|
+
* uma pessoa" - e a conclusão não segue da premissa: quem lê um relatório que só fala de valores
|
|
66
|
+
* nomeáveis não tem como distinguir "não sobrou nada" de "sobrou, e nós engolimos".
|
|
67
|
+
*
|
|
68
|
+
* ENTÃO ELE DIZ O VALOR E A LINHA, E PARA AÍ. Nenhum nome nosso é proposto: `--ds-<algo>`
|
|
69
|
+
* inventado aqui entraria no código dele sem uma pessoa ter decidido, e este comando roda depois
|
|
70
|
+
* de CADA escrita do agente - é o pior lugar do produto para inventar vocabulário. A seção 0 da
|
|
71
|
+
* jornada é literal: sem nome no sistema DELE, o valor fica onde está.
|
|
72
|
+
*/
|
|
73
|
+
const unnamed = d.findings.filter((f) => !nameToWrite(f));
|
|
60
74
|
const phantoms = d.files.flatMap((f) => f.phantoms ?? []);
|
|
61
75
|
// Every check leaves one line in the ledger - INCLUDING the clean ones,
|
|
62
76
|
// because a fix is itself a write, so the clean re-check of a file that was
|
|
@@ -70,21 +84,60 @@ async function report(root, filePath) {
|
|
|
70
84
|
named: named.length,
|
|
71
85
|
phantoms: phantoms.length,
|
|
72
86
|
});
|
|
73
|
-
|
|
74
|
-
// no token to move to, so the only honest advice is "ask a person" - and
|
|
75
|
-
// saying that after every edit trains the reader to skip the block.
|
|
76
|
-
if (named.length === 0 && phantoms.length === 0)
|
|
87
|
+
if (named.length === 0 && phantoms.length === 0 && unnamed.length === 0)
|
|
77
88
|
return greet(root, rel);
|
|
78
89
|
// A report IS the evidence the greeting exists to provide, so it counts as the
|
|
79
90
|
// introduction. Otherwise a project whose first file had drift would get the
|
|
80
91
|
// "it is live" sentence afterwards, telling somebody who just watched it work.
|
|
81
92
|
await markGreeted(root);
|
|
93
|
+
/**
|
|
94
|
+
* QUANDO NÃO HÁ NADA A FAZER NAQUELE ARQUIVO, O RELATÓRIO É DE DUAS LINHAS - e isto é o que
|
|
95
|
+
* mantém o comando instalado.
|
|
96
|
+
*
|
|
97
|
+
* MEDIDO pela revisão de DX no fecho da etapa 11, rodando o scanner deste binário sobre os 38
|
|
98
|
+
* arquivos de uma biblioteca real com o vocabulário mais generoso possível (as 94 custom
|
|
99
|
+
* properties que o próprio repositório declara): **20 arquivos (53%) disparam pelo menos um
|
|
100
|
+
* achado sem nome, e 18 desses 20 têm ZERO achado nomeado** - o relatório inteiro seria o bloco
|
|
101
|
+
* novo. O cabeçalho deste arquivo é explícito sobre o risco: *"a hook that speaks on every edit
|
|
102
|
+
* is noise, and noise is what gets a hook removed"*.
|
|
103
|
+
*
|
|
104
|
+
* Então a MESMA informação sai em duas formas. Sozinha, ela é uma frase - o valor, a linha, e a
|
|
105
|
+
* ação, que é não agir. Ao lado de algo acionável, ela é um bloco, porque aí o contraste com a
|
|
106
|
+
* seta do bloco de cima é o que ensina a diferença entre os dois conjuntos.
|
|
107
|
+
*
|
|
108
|
+
* O que NÃO está resolvido, e é decisão dele: a REPETIÇÃO no tempo. Um valor cuja resposta certa
|
|
109
|
+
* é "deixe como está" volta a ser dito na próxima escrita naquele arquivo, porque não existe
|
|
110
|
+
* memória por linha e criar uma é comportamento novo. Está medido em `steps/11-fora-do-escopo.md`.
|
|
111
|
+
*/
|
|
112
|
+
const sample = unnamed
|
|
113
|
+
.slice(0, 3)
|
|
114
|
+
.map((f) => `line ${f.line} ${f.literal}`)
|
|
115
|
+
.join(", ");
|
|
116
|
+
if (named.length === 0 && phantoms.length === 0)
|
|
117
|
+
return [
|
|
118
|
+
`${rel} - ${unnamed.length} value${unnamed.length === 1 ? "" : "s"} here ${unnamed.length === 1 ? "has" : "have"} no name in this system, and nothing to replace ${unnamed.length === 1 ? "it" : "them"} with: ${sample}${unnamed.length > 3 ? `, +${unnamed.length - 3} more` : ""}.`,
|
|
119
|
+
"Do not invent a name - leave them, or ask the person what they would call it.",
|
|
120
|
+
].join("\n");
|
|
82
121
|
const lines = [`${rel} - checked against ${table.name ?? table.slug}.`];
|
|
83
122
|
if (named.length > 0) {
|
|
84
123
|
lines.push("", "Values written by hand that this system already has a name for:", ...named
|
|
85
124
|
.slice(0, 20)
|
|
86
125
|
.map((f) => ` line ${f.line} ${f.literal} → ${nameToWrite(f)}`), "", "Replace them now, while you still have this file in mind.");
|
|
87
126
|
}
|
|
127
|
+
if (unnamed.length > 0) {
|
|
128
|
+
/**
|
|
129
|
+
* O TÍTULO CARREGA A PROIBIÇÃO, e não a terceira frase de um parágrafo - achado da revisão de
|
|
130
|
+
* DX: a lista tinha a MESMA forma da lista acionável de cima (`Values written by hand that…`,
|
|
131
|
+
* uma linha por achado), e um agente que acabou de aprender *"lista de valores → proponha o
|
|
132
|
+
* token"* recebia aqui uma lista igual com a única frase que interrompe esse reflexo escondida
|
|
133
|
+
* no fim.
|
|
134
|
+
*/
|
|
135
|
+
lines.push("", "Not yet named - do NOT invent a name for these:", ...unnamed.slice(0, 20).map((f) => ` line ${f.line} ${f.literal}`),
|
|
136
|
+
/** O QUE FOI CORTADO É DITO - medido num `.css` real: 20 de 252, e as 232 sumiam caladas. */
|
|
137
|
+
...(unnamed.length > 20
|
|
138
|
+
? [` … and ${unnamed.length - 20} more in this file`]
|
|
139
|
+
: []), "", "This system declares no name for them, so there is nothing to replace them with.", "Leave them as they are, or tell the person the value and what THEY would call it.");
|
|
140
|
+
}
|
|
88
141
|
if (phantoms.length > 0) {
|
|
89
142
|
lines.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms.slice(0, 20).map((p) => ` line ${p.line} ${p.name}`), "", "Use a name the system has, or say which value you need and what you would call it. Do NOT invent a token.");
|
|
90
143
|
}
|
package/dist/commands/refit.js
CHANGED
|
@@ -8,7 +8,7 @@ import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalle
|
|
|
8
8
|
import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
9
9
|
import { flavourResolver } from "../styles-flavour.js";
|
|
10
10
|
import { projectTongue } from "../their-tongue.js";
|
|
11
|
-
import { recordWritten } from "../written.js";
|
|
11
|
+
import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
|
|
12
12
|
/** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
|
|
13
13
|
* a folder holding only refit artifacts doesn't count). */
|
|
14
14
|
/** True when the system is actually installed (tokens.css present). */
|
|
@@ -122,6 +122,26 @@ export async function refit(file, opts) {
|
|
|
122
122
|
const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
|
|
123
123
|
/** O VOCABULÁRIO DELE - o `refit` reescreve o componente e não passava pela porta. */
|
|
124
124
|
await projectTongue(root, slug), await installedThemeVars(root, slug));
|
|
125
|
+
/**
|
|
126
|
+
* A EDICAO DELE VENCE A REESCRITA (T7) - a leitura que faltava, a mesma do `generate`.
|
|
127
|
+
*
|
|
128
|
+
* Este comando REESCREVE um componente que a pessoa ja' tem, que e' exatamente a situacao em
|
|
129
|
+
* que o arquivo dela mais provavelmente foi tocado. Ele gravava o fingerprint e nunca o lia.
|
|
130
|
+
*/
|
|
131
|
+
const edited = await theirEdits(join(root, "_synthesisui", "ds", slug), res.name, compDir);
|
|
132
|
+
if (edited && edited.length > 0 && !opts.force) {
|
|
133
|
+
console.log("");
|
|
134
|
+
console.log(
|
|
135
|
+
/**
|
|
136
|
+
* O COMANDO QUE RETOMA E' O QUE ELA ACABOU DE RODAR - `refit` recebe o CAMINHO do
|
|
137
|
+
* arquivo-fonte, nunca o nome do componente. Sugerir `refit ${res.name}` devolveria
|
|
138
|
+
* `Could not read "<nome>"`, e uma saida que nao funciona e' pior que nenhuma saida:
|
|
139
|
+
* a pessoa fez o que a tela mandou e levou uma segunda recusa.
|
|
140
|
+
*/
|
|
141
|
+
keptLine(res.name, edited, `npx synthesisui refit ${file}${opts.name ? ` --name ${opts.name}` : ""} --force`));
|
|
142
|
+
console.log(` The recipe is saved in your system's draft either way - only the .tsx was not written.`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
125
145
|
for (const f of files) {
|
|
126
146
|
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
127
147
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -582,6 +582,24 @@ export async function remeasure(args) {
|
|
|
582
582
|
console.log(body(`${out.written ?? 0} of ${out.total ?? 0} components written.`));
|
|
583
583
|
for (const note of out.notes ?? [])
|
|
584
584
|
console.log(body(paint.faint(note)));
|
|
585
|
+
/**
|
|
586
|
+
* A RECUSA, DITA - e este era o unico numero da tela sem explicacao.
|
|
587
|
+
*
|
|
588
|
+
* O QUE O CLIENTE VIA (medido em 10/09): `60 of 62 components written.` e mais nada. Os dois
|
|
589
|
+
* que faltaram estavam nomeados na resposta, com o motivo, e o comando descartava o campo. Quem
|
|
590
|
+
* le "60 de 62" so' pode concluir uma coisa - que a esteira perdeu dois - e vai procurar um
|
|
591
|
+
* defeito que nao existe, ou pior, nao vai.
|
|
592
|
+
*
|
|
593
|
+
* A COPY JA' EXISTIA: `because` e' a frase do servidor, e reescreve-la aqui seria a segunda
|
|
594
|
+
* versao da mesma explicacao. A primeira coisa que a segunda faz e' discordar da primeira.
|
|
595
|
+
*/
|
|
596
|
+
const refused = out.refused ?? [];
|
|
597
|
+
if (refused.length > 0) {
|
|
598
|
+
console.log("");
|
|
599
|
+
console.log(body(`${refused.length} ${refused.length === 1 ? "component was" : "components were"} refused - ${refused.length === 1 ? "it is" : "they are"} not lost, ${refused.length === 1 ? "it was" : "they were"} not written:`));
|
|
600
|
+
for (const r of refused)
|
|
601
|
+
console.log(body(paint.strong(` ✕ ${r.name} - ${r.because}`)));
|
|
602
|
+
}
|
|
585
603
|
/**
|
|
586
604
|
* A RECUSA POR ESCOPO, DITA - e não como um número que some.
|
|
587
605
|
*
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
3
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
4
4
|
import { fetchTemplate } from "../registry.js";
|
|
5
|
+
import { installedThemeCss, installedTokensCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
|
|
5
6
|
import { inTheirTongue, projectTongue } from "../their-tongue.js";
|
|
7
|
+
import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
|
|
8
|
+
/**
|
|
9
|
+
* O QUE O ARQUIVO DIZ TAMBÉM É COBRANÇA - e ela é a metade que FICA no repositório dele.
|
|
10
|
+
*
|
|
11
|
+
* O DEFEITO, achado pela revisão de DX no fecho da etapa 11: o fecho no terminal parou de mandar
|
|
12
|
+
* instalar a folha quando a tradução a tornou desnecessária, e o `.tsx` que acabou de ser escrito
|
|
13
|
+
* continuava abrindo com *"Ensure the DS tokens.css is imported"* e *"Keep the … ds-* classes
|
|
14
|
+
* (on-system…)"*. O terminal role uma vez; o comentário fica no arquivo que o time dele abre
|
|
15
|
+
* depois, sem o terminal por perto para desmentir. É a mesma contradição de duas frases que o
|
|
16
|
+
* critério conserta, um arquivo adiante.
|
|
17
|
+
*
|
|
18
|
+
* QUEM SABE A RESPOSTA É ESTE LADO, e é por isso que a linha sai aqui e não no servidor: a
|
|
19
|
+
* pergunta é *"o REPOSITÓRIO dele ainda precisa da folha?"*, e o codegen do servidor não conhece o
|
|
20
|
+
* repositório dele. Lá o header é escrito completo, que é a resposta certa para quem não traduziu
|
|
21
|
+
* nada.
|
|
22
|
+
*
|
|
23
|
+
* E O PAR NÃO É VIGIADO POR ACIDENTE: `apps/web/src/lib/ds/the-page-header-can-be-uncharged.spec.ts`
|
|
24
|
+
* assere que o header do servidor traz exatamente estas duas marcas. Se alguém reescrever a frase
|
|
25
|
+
* lá, aquele spec fica VERMELHO - em vez de este filtro passar a não casar em silêncio, que é
|
|
26
|
+
* exatamente como um `replace` por string falha aqui.
|
|
27
|
+
*/
|
|
28
|
+
const SHEET_INSTRUCTION = /tokens\.css is imported|\(on-system/;
|
|
29
|
+
function withoutTheSheetInstruction(code) {
|
|
30
|
+
return code
|
|
31
|
+
.split("\n")
|
|
32
|
+
.filter((line) => !SHEET_INSTRUCTION.test(line))
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
6
35
|
/**
|
|
7
36
|
* Materializes a whole page from a DS template into the project (hybrid
|
|
8
37
|
* codegen-first): the server codegens deterministic files, we write them, and
|
|
@@ -28,6 +57,36 @@ export async function template(slug, name, opts) {
|
|
|
28
57
|
const defaultDir = join("templates", asName ?? name);
|
|
29
58
|
const pageRel = opts.out ?? join(defaultDir, pageFile.filename);
|
|
30
59
|
const pageDir = dirname(join(root, pageRel));
|
|
60
|
+
/**
|
|
61
|
+
* A EDICAO DELE VENCE A REESCRITA (T7) - a MESMA guarda do `component`, no comando que a nao
|
|
62
|
+
* tinha.
|
|
63
|
+
*
|
|
64
|
+
* MEDIDO EM 10/09: `component.ts` consultava o registro do que nos escrevemos em seis pontos e
|
|
65
|
+
* `template.ts` em nenhum. E este comando escreve MAIS: uma pagina inteira, mais a folha
|
|
66
|
+
* co-locada. Alguem que traz um template, ajusta o texto, e roda de novo para pegar uma correcao
|
|
67
|
+
* perdia o ajuste - sem aviso, sem `--force`, sem uma frase.
|
|
68
|
+
*
|
|
69
|
+
* A MESMA CHAVE PARA OS DOIS LADOS: `templates/<nome>` e' o que a leitura procura e o que a
|
|
70
|
+
* escrita grava, entao o fingerprint de hoje e' lido pela rodada de amanha. Sem `.lock`
|
|
71
|
+
* (template trazido antes do install) nao ha' memoria, e o comportamento de sempre continua.
|
|
72
|
+
*/
|
|
73
|
+
const slugDir = join(root, "_synthesisui", "ds", slug);
|
|
74
|
+
const entry = relative(root, pageDir).split(/[\\/]/).join("/");
|
|
75
|
+
const editedHere = await theirEdits(slugDir, entry, pageDir);
|
|
76
|
+
if (editedHere && editedHere.length > 0 && !opts.force) {
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(keptLine(entry, editedHere,
|
|
79
|
+
/**
|
|
80
|
+
* O RETAKE CARREGA `--out`, senao ele escreve em OUTRO LUGAR.
|
|
81
|
+
*
|
|
82
|
+
* Sem esta parte, quem rodou `template landing --out app/page.tsx` e seguiu a instrucao
|
|
83
|
+
* da recusa escreveria em `templates/landing/page.tsx`: o arquivo que ele editou fica
|
|
84
|
+
* intocado, nasce um segundo, e nada diz que aconteceu. Uma saida que escreve no lugar
|
|
85
|
+
* errado e' pior que uma recusa sem saida.
|
|
86
|
+
*/
|
|
87
|
+
`npx synthesisui template ${slug} ${name}${asName ? ` --as ${asName}` : ""}${opts.out ? ` --out ${opts.out}` : ""} --force`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
31
90
|
/**
|
|
32
91
|
* NENHUMA MATERIALIZAÇÃO VAZA VOCABULÁRIO INTERNO (INV-VOLTA-02) - a mesma porta do
|
|
33
92
|
* `component`. Uma página inteira saía com `var(--ds-*)` cru enquanto um componente avulso
|
|
@@ -39,28 +98,84 @@ export async function template(slug, name, opts) {
|
|
|
39
98
|
let named = 0;
|
|
40
99
|
let inlined = 0;
|
|
41
100
|
const still = new Set();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
101
|
+
/**
|
|
102
|
+
* TRADUZ TUDO ANTES DE ESCREVER NADA, porque a pergunta *"esta pagina precisa da nossa folha?"*
|
|
103
|
+
* se responde sobre os bytes TRADUZIDOS - e a resposta decide o que vai a disco, nao so' o que o
|
|
104
|
+
* terminal diz.
|
|
105
|
+
*
|
|
106
|
+
* A ordem anterior escrevia e depois media. Funcionava enquanto a medicao servia so' ao fecho no
|
|
107
|
+
* terminal; no momento em que ela passou a decidir uma linha do ARQUIVO, escrever primeiro era
|
|
108
|
+
* gravar a decisao errada e corrigi-la na frase seguinte.
|
|
109
|
+
*/
|
|
110
|
+
const pieces = [];
|
|
111
|
+
const translate = (code) => {
|
|
112
|
+
const spoken = speak(code);
|
|
113
|
+
if (!spoken)
|
|
114
|
+
return code;
|
|
115
|
+
named += spoken.named;
|
|
116
|
+
inlined += spoken.inlined;
|
|
117
|
+
for (const l of spoken.left)
|
|
48
118
|
still.add(l);
|
|
119
|
+
return spoken.css;
|
|
120
|
+
};
|
|
121
|
+
pieces.push({
|
|
122
|
+
rel: pageRel,
|
|
123
|
+
filename: pageRel.split(/[\\/]/).pop() ?? pageFile.filename,
|
|
124
|
+
content: translate(pageFile.code),
|
|
125
|
+
});
|
|
126
|
+
for (const f of siblings)
|
|
127
|
+
pieces.push({
|
|
128
|
+
rel: join(dirname(pageRel), f.filename),
|
|
129
|
+
filename: f.filename,
|
|
130
|
+
content: translate(f.code),
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* A FOLHA SÓ É COBRADA QUANDO ESTA PÁGINA PRECISA DELA (A2 da etapa 11) - ver
|
|
134
|
+
* `sheet-needed.ts`, a mesma porta que o `component` usa.
|
|
135
|
+
*
|
|
136
|
+
* O DEFEITO, medido em 10/09: o comando já contava em `still` quantas referências sobraram
|
|
137
|
+
* apontando para a nossa folha depois da tradução, imprimia o número, e três linhas abaixo
|
|
138
|
+
* mandava instalar a folha e colar um `@import` - inclusive quando o número era ZERO. Uma página
|
|
139
|
+
* que renderiza igual sem os dois passava a pedi-los, e pedir cria exatamente a dependência que
|
|
140
|
+
* a plataforma promete não criar: a gente é referência, não dependência.
|
|
141
|
+
*
|
|
142
|
+
* E A PERGUNTA É MAIOR QUE `still`, que é por que ela não é feita com ele: a folha entrega as
|
|
143
|
+
* variáveis, as classes que só o `@theme` gera E as classes das receitas (`.ds-hero`). Uma
|
|
144
|
+
* página veste as três, e cortar o setup contando só as variáveis entregaria uma página pelada.
|
|
145
|
+
*
|
|
146
|
+
* SEM TRADUÇÃO, NADA MUDA: sem o mapa do `.lock` o `--ds-*` cru está no arquivo, a folha é o
|
|
147
|
+
* caminho, e a cobrança sai inteira como sempre saiu.
|
|
148
|
+
*/
|
|
149
|
+
const need = tongue
|
|
150
|
+
? whatOnlyTheSheetResolves({
|
|
151
|
+
source: pieces.map((f) => f.content).join("\n"),
|
|
152
|
+
themeCss: await installedThemeCss(root, slug),
|
|
153
|
+
sheetCss: await installedTokensCss(root, slug),
|
|
154
|
+
theirNames: [...tongue.names.values()],
|
|
155
|
+
})
|
|
156
|
+
: null;
|
|
157
|
+
const chargesTheSheet = !need || need.needed;
|
|
158
|
+
await mkdir(pageDir, { recursive: true });
|
|
159
|
+
/** OS BYTES QUE FORAM A DISCO, para o fingerprint lembrar EXATAMENTE o que escrevemos. */
|
|
160
|
+
const landed = [];
|
|
161
|
+
for (const piece of pieces) {
|
|
162
|
+
const content = chargesTheSheet
|
|
163
|
+
? piece.content
|
|
164
|
+
: withoutTheSheetInstruction(piece.content);
|
|
165
|
+
await writeFile(join(root, piece.rel), content, "utf8");
|
|
166
|
+
landed.push({ filename: piece.filename, content });
|
|
167
|
+
console.log(piece.rel === pageRel
|
|
168
|
+
? `✓ wrote ${piece.rel} (${slug} v${generated.version})`
|
|
169
|
+
: `✓ wrote ${piece.rel}`);
|
|
49
170
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for (const l of spoken.left)
|
|
59
|
-
still.add(l);
|
|
60
|
-
}
|
|
61
|
-
await writeFile(join(root, rel), spoken ? spoken.css : f.code, "utf8");
|
|
62
|
-
console.log(`✓ wrote ${rel}`);
|
|
63
|
-
}
|
|
171
|
+
/**
|
|
172
|
+
* E O REGISTRO E' GRAVADO - a outra metade da guarda, e sem ela a primeira nunca dispara.
|
|
173
|
+
*
|
|
174
|
+
* A leitura la' em cima procura o fingerprint desta escrita. Ler sem gravar seria uma guarda que
|
|
175
|
+
* passa em todo teste e nunca protege ninguem: a promessa so' existe quando as duas pontas
|
|
176
|
+
* fecham.
|
|
177
|
+
*/
|
|
178
|
+
await recordWritten(slugDir, entry, landed);
|
|
64
179
|
if (named > 0 || inlined > 0) {
|
|
65
180
|
console.log(` ${named} reference${named === 1 ? "" : "s"} now speak${named === 1 ? "s" : ""} the name YOUR code gives the value${inlined > 0 ? `, and ${inlined} carr${inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
|
|
66
181
|
if (still.size > 0) {
|
|
@@ -72,8 +187,28 @@ export async function template(slug, name, opts) {
|
|
|
72
187
|
console.log("Next steps:");
|
|
73
188
|
console.log(` • use it in a route, e.g. ${join(config.pagesDir, "page.tsx")}:`);
|
|
74
189
|
console.log(` import Page from "@/${defaultDir.replace(/\\/g, "/")}/${pageFile.filename.replace(/\.tsx$/, "")}";`);
|
|
75
|
-
|
|
76
|
-
|
|
190
|
+
if (chargesTheSheet) {
|
|
191
|
+
/** E ELE DIZ O QUE PEDE A FOLHA - um setup cobrado sem motivo dito ensina a ignorar o próximo. */
|
|
192
|
+
if (need)
|
|
193
|
+
console.log(` • what still needs the sheet here: ${[...need.recipeClasses, ...need.classes, ...need.variables].slice(0, 3).join(", ")}`);
|
|
194
|
+
console.log(` • ensure the DS is installed: synthesisui add ${slug} (provides tokens.css)`);
|
|
195
|
+
console.log(` • @import "_synthesisui/ds/${slug}/tokens.css" in your global CSS`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(` • nothing to install: every value in this page is a name YOUR code declares, so it renders without our stylesheet`);
|
|
199
|
+
}
|
|
77
200
|
console.log(" • refine the file: wire real data, split into components, swap placeholders");
|
|
78
|
-
|
|
201
|
+
/**
|
|
202
|
+
* E A LINHA DO ESCOPO SÓ SAI ONDE ELA É VERDADE - medido rodando o comando em 10/09.
|
|
203
|
+
*
|
|
204
|
+
* Ela mandava *"keep the data-ds wrapper and the ds-* classes (stays on-system)"* sempre, e no
|
|
205
|
+
* caminho isolado a saída ficava contradizendo a linha logo acima: *"nothing to install… it
|
|
206
|
+
* renders without our stylesheet"* e, uma linha depois, uma instrução para manter um escopo que
|
|
207
|
+
* não resolve nada naquele arquivo. É o mesmo defeito que este bloco existe para consertar - duas
|
|
208
|
+
* frases que não podem ser verdadeiras ao mesmo tempo -, e a medição já responde qual das duas é:
|
|
209
|
+
* chegar aqui com `chargesTheSheet` falso significa zero variável nossa e zero classe nossa no
|
|
210
|
+
* que acabou de ser escrito.
|
|
211
|
+
*/
|
|
212
|
+
if (chargesTheSheet)
|
|
213
|
+
console.log(` • keep the data-ds="${slug}" wrapper and the ds-* / layout classes (stays on-system)`);
|
|
79
214
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -211,6 +211,56 @@ async function theOnlyInstalled(root) {
|
|
|
211
211
|
throw new RegistryError("No design system is installed here - run `synthesisui add <slug>` first.");
|
|
212
212
|
throw new RegistryError(`More than one system is installed here (${slugs.join(", ")}) - name the one to update: \`synthesisui upgrade <slug>\`.`);
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* O QUE ELE EDITOU DEPOIS DE NOS ESCREVERMOS - a mesma leitura, num lugar so'.
|
|
216
|
+
*
|
|
217
|
+
* A PROMESSA T7 e' *"kept YOUR file(s)"*, e ela vivia dentro do laco que regenera. Isso amarrou a
|
|
218
|
+
* promessa a UM caminho: o do upgrade que tem versao nova. Medido em 10/09 - com a versao igual,
|
|
219
|
+
* `upgrade` retornava antes de chegar ao laco, e a pessoa que editou tres componentes nao ouvia
|
|
220
|
+
* nada sobre eles no comando cuja funcao e' justamente dizer o que aconteceu com os arquivos dela.
|
|
221
|
+
*
|
|
222
|
+
* Extraida, a resposta e' a mesma nos tres caminhos - rematerializou, ja' estava na ultima, ou
|
|
223
|
+
* subiu de versao. Uma leitura, uma frase.
|
|
224
|
+
*/
|
|
225
|
+
async function editedByHand(root, slug, slugDir, componentsDir) {
|
|
226
|
+
const componentsRoot = join(root, componentsDir);
|
|
227
|
+
const marker = `from the "${slug}" design system`;
|
|
228
|
+
const writtenMap = await readWritten(slugDir);
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const entry of await readdir(componentsRoot).catch(() => [])) {
|
|
231
|
+
let head = "";
|
|
232
|
+
try {
|
|
233
|
+
head = (await readFile(join(componentsRoot, entry, `${entry}.tsx`), "utf8")).slice(0, 300);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
continue; // not a materialized component folder
|
|
237
|
+
}
|
|
238
|
+
if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
|
|
239
|
+
continue;
|
|
240
|
+
const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
|
|
241
|
+
if (edited && edited.length > 0)
|
|
242
|
+
out.push({ entry, edited });
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* E A FRASE, DITA - nos caminhos que NAO regeneram nada.
|
|
248
|
+
*
|
|
249
|
+
* Aqui ela nao e' o relatorio de uma reescrita que respeitou o arquivo dele: e' a confirmacao de
|
|
250
|
+
* que este comando passou e nao tocou no que e' dele. E' a mesma informacao, e omiti-la nos
|
|
251
|
+
* caminhos silenciosos e' o que fazia a promessa valer so' as vezes.
|
|
252
|
+
*/
|
|
253
|
+
async function sayWhatWasKept(root, slug, slugDir) {
|
|
254
|
+
const config = await readProjectConfig(root).catch(() => null);
|
|
255
|
+
if (!config || config.target !== "next")
|
|
256
|
+
return;
|
|
257
|
+
const kept = await editedByHand(root, slug, slugDir, config.componentsDir);
|
|
258
|
+
if (kept.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
console.log("");
|
|
261
|
+
for (const k of kept)
|
|
262
|
+
console.log(body(keptLine(k.entry, k.edited, `npx synthesisui component ${slug} ${k.entry}`)));
|
|
263
|
+
}
|
|
214
264
|
export async function upgrade(asked, opts) {
|
|
215
265
|
const base = resolveRegistry(opts.registry);
|
|
216
266
|
const root = opts.dir ?? process.cwd();
|
|
@@ -263,6 +313,8 @@ export async function upgrade(asked, opts) {
|
|
|
263
313
|
* medido.
|
|
264
314
|
*/
|
|
265
315
|
const measured = await checkAfterWriting(root, `v${installed}`);
|
|
316
|
+
/** ESTE CAMINHO REESCREVE OS ARTEFATOS - e o que e' dele fica onde esta'. Ver `sayWhatWasKept`. */
|
|
317
|
+
await sayWhatWasKept(root, slug, slugDir);
|
|
266
318
|
await reportWhatIsLeft(root, {
|
|
267
319
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
268
320
|
...(measured > 0 ? { justMeasured: measured } : {}),
|
|
@@ -271,6 +323,12 @@ export async function upgrade(asked, opts) {
|
|
|
271
323
|
}
|
|
272
324
|
if (!opts.force) {
|
|
273
325
|
console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
|
|
326
|
+
/**
|
|
327
|
+
* E AQUI TAMBEM, que e' o caminho mais percorrido de todos: sem gap de versao, este comando
|
|
328
|
+
* dizia uma linha e sumia. Quem editou tres componentes ficava sem saber se eles seguem sendo
|
|
329
|
+
* dele - no comando que existe para responder isso.
|
|
330
|
+
*/
|
|
331
|
+
await sayWhatWasKept(root, slug, slugDir);
|
|
274
332
|
await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
|
|
275
333
|
return;
|
|
276
334
|
}
|
|
@@ -308,8 +366,9 @@ export async function upgrade(asked, opts) {
|
|
|
308
366
|
const regenerated = [];
|
|
309
367
|
const failed = [];
|
|
310
368
|
/** Componentes que ELE editou desde que os escrevemos - mantidos intactos (T7). */
|
|
311
|
-
const kept =
|
|
312
|
-
|
|
369
|
+
const kept = await editedByHand(root, slug, slugDir, config.componentsDir);
|
|
370
|
+
/** Os que ELE editou saem da regeneracao - a leitura acima ja' os separou. */
|
|
371
|
+
const keptEntries = new Set(kept.map((k) => k.entry));
|
|
313
372
|
if (config.target === "next") {
|
|
314
373
|
const componentsRoot = join(root, config.componentsDir);
|
|
315
374
|
const marker = `from the "${slug}" design system`;
|
|
@@ -345,16 +404,13 @@ export async function upgrade(asked, opts) {
|
|
|
345
404
|
if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
|
|
346
405
|
continue;
|
|
347
406
|
/**
|
|
348
|
-
* A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `
|
|
407
|
+
* A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `editedByHand`. O fingerprint do que NÓS
|
|
349
408
|
* escrevemos foi gravado no `.lock` na hora da escrita; se o disco divergiu, ele editou, e o
|
|
350
409
|
* arquivo é DELE - fica intacto e o terminal diz qual. `null` é install de antes do
|
|
351
410
|
* fingerprint: sem memória não há veredito, e o comportamento de sempre continua.
|
|
352
411
|
*/
|
|
353
|
-
|
|
354
|
-
if (edited && edited.length > 0) {
|
|
355
|
-
kept.push({ entry, edited });
|
|
412
|
+
if (keptEntries.has(entry))
|
|
356
413
|
continue;
|
|
357
|
-
}
|
|
358
414
|
try {
|
|
359
415
|
const res = await fetchComponent(base, slug, entry);
|
|
360
416
|
const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
|
|
@@ -460,7 +516,7 @@ export async function upgrade(asked, opts) {
|
|
|
460
516
|
if (kept.length > 0) {
|
|
461
517
|
console.log("");
|
|
462
518
|
for (const k of kept)
|
|
463
|
-
console.log(body(keptLine(
|
|
519
|
+
console.log(body(keptLine(k.entry, k.edited, `npx synthesisui component ${slug} ${k.entry}`)));
|
|
464
520
|
}
|
|
465
521
|
const measured = await checkAfterWriting(root, `v${latest.version}`);
|
|
466
522
|
console.log(section("Migrate the app"));
|
package/dist/doctor/ci-format.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* checagem que telefona para casa é uma checagem que alguém desinstala.
|
|
24
24
|
*/
|
|
25
25
|
import { isOlderCli } from "../cli-version.js";
|
|
26
|
-
import {
|
|
26
|
+
import { COUNTED_DIFFERENTLY, COUNTED_DIFFERENTLY_SINCE, } from "../install-marks.js";
|
|
27
27
|
const byFileOf = (d) => {
|
|
28
28
|
const out = {};
|
|
29
29
|
for (const f of d.files)
|
|
@@ -75,10 +75,12 @@ export function compareToBaseline(d, base, now) {
|
|
|
75
75
|
* prometia o contrário desde o primeiro dia (*"um número que sobe porque o leitor melhorou não é
|
|
76
76
|
* regressão"*), e `incomparable` existia para exatamente isto.
|
|
77
77
|
*
|
|
78
|
-
* A RÉGUA É
|
|
79
|
-
* compara mudou de resposta
|
|
78
|
+
* A RÉGUA É `COUNTED_DIFFERENTLY_SINCE`, em `install-marks.ts`: a última versão em que a CONTAGEM
|
|
79
|
+
* que o CI compara mudou de resposta. Ela mora na casa das outras marcas porque é lá que está a
|
|
80
|
+
* disciplina de mantê-las - uma marca à mão sem portão é uma marca errada esperando a hora -, e
|
|
81
|
+
* não é a MESMA das outras: emprestar `CHECKER_SINCE` foi exatamente o defeito que a criou.
|
|
80
82
|
*/
|
|
81
|
-
const readerMoved = base.cli !== undefined && isOlderCli(base.cli,
|
|
83
|
+
const readerMoved = base.cli !== undefined && isOlderCli(base.cli, COUNTED_DIFFERENTLY_SINCE);
|
|
82
84
|
return {
|
|
83
85
|
worse: !scopeChanged &&
|
|
84
86
|
!readerMoved &&
|
|
@@ -95,7 +97,7 @@ export function compareToBaseline(d, base, now) {
|
|
|
95
97
|
}
|
|
96
98
|
: readerMoved
|
|
97
99
|
? {
|
|
98
|
-
incomparable: `the baseline was written by CLI ${base.cli}, and
|
|
100
|
+
incomparable: `the baseline was written by CLI ${base.cli}, and ${COUNTED_DIFFERENTLY}. The rise is in the reading, not in your code. Rewrite the line before comparing: npx synthesisui doctor --write-baseline`,
|
|
99
101
|
}
|
|
100
102
|
: {}),
|
|
101
103
|
};
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,7 @@ Options:
|
|
|
117
117
|
--instruction <s> refit: extra guidance for the adaptation
|
|
118
118
|
--dry refit: adapt and print, but save nothing
|
|
119
119
|
--force clean: apply the changes (without it, dry run)
|
|
120
|
+
component/template/generate/refit: overwrite a file YOU edited
|
|
120
121
|
upgrade: rewrite UPGRADE.md even with no version gap left
|
|
121
122
|
--from <n> upgrade --force: which older snapshot to diff from
|
|
122
123
|
--strict doctor: exit 1 when drift is found in THIS repo (for CI)
|
|
@@ -155,6 +156,21 @@ Examples:
|
|
|
155
156
|
async function main() {
|
|
156
157
|
const { positionals, flags } = parseFlags(process.argv.slice(2));
|
|
157
158
|
const [command, ...args] = positionals;
|
|
159
|
+
/**
|
|
160
|
+
* `synthesisui --version` RESPONDE A VERSAO - uma linha, exit 0.
|
|
161
|
+
*
|
|
162
|
+
* O QUE ACONTECIA (medido em 10/09): `node dist/index.js --version` imprimia as 117 linhas do
|
|
163
|
+
* help e saia 0. Quem pergunta a versao de uma ferramenta - uma pessoa num terminal, um script
|
|
164
|
+
* de CI, um relatorio de bug - recebia a lista de comandos, e a unica forma de descobrir a
|
|
165
|
+
* versao era abrir o `package.json` da instalacao.
|
|
166
|
+
*
|
|
167
|
+
* SEM COMANDO, sempre: `--version <n>` continua sendo flag do `add`, do `component` e do
|
|
168
|
+
* `template`, e la ela chega com um comando na frente. A distincao e' posicional, nao de nome.
|
|
169
|
+
*/
|
|
170
|
+
if (!command && flags.version !== undefined) {
|
|
171
|
+
console.log(CLI_VERSION);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
158
174
|
if (!command || flags.help || command === "help") {
|
|
159
175
|
/**
|
|
160
176
|
* A PRIMEIRA LINHA FALA DO PROJETO ONDE ELA FOI RODADA - ver `where-you-are.ts`.
|
|
@@ -484,6 +500,8 @@ async function main() {
|
|
|
484
500
|
target,
|
|
485
501
|
version,
|
|
486
502
|
as: as_,
|
|
503
|
+
/** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
|
|
504
|
+
force: flags.force === true,
|
|
487
505
|
});
|
|
488
506
|
break;
|
|
489
507
|
}
|
|
@@ -579,6 +597,8 @@ async function main() {
|
|
|
579
597
|
instruction: typeof flags.instruction === "string" ? flags.instruction : undefined,
|
|
580
598
|
support: typeof flags.support === "string" ? flags.support : undefined,
|
|
581
599
|
dry: flags.dry === true,
|
|
600
|
+
/** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
|
|
601
|
+
force: flags.force === true,
|
|
582
602
|
});
|
|
583
603
|
break;
|
|
584
604
|
}
|
|
@@ -681,7 +701,14 @@ async function main() {
|
|
|
681
701
|
}
|
|
682
702
|
const ds = typeof flags.ds === "string" ? flags.ds : undefined;
|
|
683
703
|
const name = typeof flags.name === "string" ? flags.name : undefined;
|
|
684
|
-
await generate(description, {
|
|
704
|
+
await generate(description, {
|
|
705
|
+
registry,
|
|
706
|
+
dir,
|
|
707
|
+
ds,
|
|
708
|
+
name,
|
|
709
|
+
/** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
|
|
710
|
+
force: flags.force === true,
|
|
711
|
+
});
|
|
685
712
|
break;
|
|
686
713
|
}
|
|
687
714
|
default:
|
package/dist/install-marks.js
CHANGED
|
@@ -170,7 +170,24 @@
|
|
|
170
170
|
* O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
|
|
171
171
|
* sistema, em vez de só receber as regras e adivinhar o resto.
|
|
172
172
|
*/
|
|
173
|
-
|
|
173
|
+
/**
|
|
174
|
+
* 0.16.403 -> 0.16.412 em 10/09, e o SIM é sobre TRÊS coisas que caem na pasta dele:
|
|
175
|
+
*
|
|
176
|
+
* `component` sem `add` sem `.lock`, o vocabulário do repositório dele passa a ser derivado
|
|
177
|
+
* em memória do sistema lido no registry. O `.tsx` que o CLI de ontem
|
|
178
|
+
* escrevia com `var(--ds-*)` cru sai com o nome que o código DELE dá
|
|
179
|
+
* àquele valor - e a nossa folha deixa de ser pré-requisito para
|
|
180
|
+
* trazer um componente.
|
|
181
|
+
* `template` com T7 a página materializada grava o fingerprint no `.lock` e passa a
|
|
182
|
+
* recusar a reescrita do que ele editou à mão. Campo novo no arquivo
|
|
183
|
+
* que o time dele commita.
|
|
184
|
+
* `generate` e `refit` gravavam o fingerprint e nunca o liam; agora recusam por edição
|
|
185
|
+
* dele, então o conjunto de bytes que uma rodada deixa é outro.
|
|
186
|
+
*
|
|
187
|
+
* O que o cliente ganha ao rodar `upgrade`: os componentes que ele já tem voltam a ser reescritos
|
|
188
|
+
* pelo materializador que respeita a edição dele em TODO comando, e não só em dois.
|
|
189
|
+
*/
|
|
190
|
+
export const MATERIALISER_SINCE = "0.16.412";
|
|
174
191
|
/**
|
|
175
192
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
176
193
|
*
|
|
@@ -258,7 +275,37 @@ export const MATERIALISER_SINCE = "0.16.403";
|
|
|
258
275
|
* não via, 14% e 16% das declarações de espaço/raio. Um hook pinado antes desta versão aconselha
|
|
259
276
|
* MENOS, que é a régua desta marca.
|
|
260
277
|
*/
|
|
261
|
-
|
|
278
|
+
/**
|
|
279
|
+
* A ÚLTIMA VERSÃO EM QUE A CONTAGEM QUE O CI COMPARA MUDOU DE RESPOSTA - a régua da catraca, e ela
|
|
280
|
+
* mora aqui porque é onde a disciplina mora.
|
|
281
|
+
*
|
|
282
|
+
* ELA ERA `CHECKER_SINCE`, e as duas perguntas não são a mesma. Aquela significa *"o que o hook
|
|
283
|
+
* ENTREGA mudou"*, e sobe também quando ele passa a DIZER algo novo sobre os mesmos achados - o que
|
|
284
|
+
* aconteceu em 10/09. Com a régua emprestada, aquela subida declararia INCOMPARÁVEL todo baseline
|
|
285
|
+
* gravado entre as duas versões: a catraca pararia de reprovar regressão de verdade até o time
|
|
286
|
+
* regravar a linha, e a frase abaixo diria a ele um motivo que não é o dele. Foi o caso negativo do
|
|
287
|
+
* `doctor/ci-format.spec.ts` que acendeu isso, sozinho.
|
|
288
|
+
*
|
|
289
|
+
* A PERGUNTA DO PASSO 1 GANHA UMA SEGUNDA METADE por causa dela, e as duas se fazem no mesmo
|
|
290
|
+
* vermelho do fingerprint do CHECKER (que já inclui `doctor/scan.ts`, quem conta):
|
|
291
|
+
*
|
|
292
|
+
* o hook aconselha diferente? -> `CHECKER_SINCE`
|
|
293
|
+
* a CONTAGEM mudou de valor? -> `COUNTED_DIFFERENTLY_SINCE`, e a FRASE junto
|
|
294
|
+
*
|
|
295
|
+
* A frase viaja ao lado da versão porque as duas são uma coisa só: quem move a marca troca a
|
|
296
|
+
* explicação no mesmo lugar, em vez de deixar o CI citando a causa da marca anterior.
|
|
297
|
+
*/
|
|
298
|
+
export const COUNTED_DIFFERENTLY_SINCE = "0.16.408";
|
|
299
|
+
export const COUNTED_DIFFERENTLY = "this run counts every length in a shorthand - that version counted only the first";
|
|
300
|
+
/**
|
|
301
|
+
* 0.16.408 -> 0.16.413 em 10/09: o hook passa a DIZER o valor que nada nomeia. Ele silenciava toda
|
|
302
|
+
* deriva sem token de destino - a decisão estava escrita como "não vale interromper, porque o único
|
|
303
|
+
* conselho honesto é pergunte a uma pessoa" -, e o silêncio é a única resposta errada disponível:
|
|
304
|
+
* quem lê um relatório que só fala de valores nomeáveis não distingue "não sobrou nada" de "sobrou,
|
|
305
|
+
* e nós engolimos". Agora sai `line 12 #ff00aa`, sem uma única proposta de nome nosso. Um hook
|
|
306
|
+
* pinado antes desta versão aconselha MENOS, que é a régua desta marca.
|
|
307
|
+
*/
|
|
308
|
+
export const CHECKER_SINCE = "0.16.413";
|
|
262
309
|
/**
|
|
263
310
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
264
311
|
*
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* ESTA PASTA E' UM PROJETO? - a pergunta que todo comando que ESCREVE no repositorio dele
|
|
6
|
+
* deveria fazer antes da primeira escrita.
|
|
7
|
+
*
|
|
8
|
+
* O QUE O CLIENTE VIVIA: `connect` rodado por engano no `~` ou num diretorio recem-criado
|
|
9
|
+
* escrevia `.claude/settings.json`, `CLAUDE.md`, `.mcp.json` e a pasta de governanca ali mesmo -
|
|
10
|
+
* seis escritas, medidas em 10/09, nenhuma delas precedida de uma checagem. Ninguem percebe no
|
|
11
|
+
* dia; percebe semanas depois, quando um `CLAUDE.md` orfao na home passa a instruir todo agente
|
|
12
|
+
* que abre naquela pasta.
|
|
13
|
+
*
|
|
14
|
+
* O CRITERIO E' O DO ECOSSISTEMA, nao um nosso: um `package.json` nesta pasta ou acima dela. A
|
|
15
|
+
* subida para no diretorio que carrega o `.git`, porque a raiz do repositorio e' onde o projeto
|
|
16
|
+
* acaba - continuar subindo alcancaria a home de quem roda, que e' exatamente o acidente que isto
|
|
17
|
+
* existe para recusar.
|
|
18
|
+
*
|
|
19
|
+
* GENERALIDADE: nao pergunta por Next, por React nem pela nossa pasta. Um projeto que ainda nao
|
|
20
|
+
* instalou nada e' um projeto; uma pasta vazia dentro de um repositorio Node tambem e', porque o
|
|
21
|
+
* manifesto de cima responde por ela.
|
|
22
|
+
*/
|
|
23
|
+
export async function projectRootFrom(dir) {
|
|
24
|
+
/**
|
|
25
|
+
* DENTRO DE UMA DEPENDENCIA NAO E' O PROJETO DELE - e' o projeto de outra pessoa, baixado.
|
|
26
|
+
*
|
|
27
|
+
* Um `cd` de distancia do acidente: `node_modules/<lib>` tem `package.json`, entao a subida
|
|
28
|
+
* parava ali e o comando escrevia a fiacao do agente dentro de uma dependencia - que some no
|
|
29
|
+
* proximo `npm ci`, levando junto tudo que ele configurou.
|
|
30
|
+
*/
|
|
31
|
+
if (dir.split(/[\\/]/).includes("node_modules"))
|
|
32
|
+
return null;
|
|
33
|
+
/**
|
|
34
|
+
* E A HOME E' O SEGUNDO TETO, ao lado da raiz do repositorio.
|
|
35
|
+
*
|
|
36
|
+
* O `.git` sozinho nao fechava o acidente que este modulo existe para recusar: numa maquina
|
|
37
|
+
* onde alguem rodou `npm init` na home - acontece -, uma pasta solta em `~/qualquer-coisa` sem
|
|
38
|
+
* git subia ate' `~`, encontrava aquele manifesto, e o comando escrevia `CLAUDE.md`,
|
|
39
|
+
* `.mcp.json` e a casa da governanca na HOME da pessoa.
|
|
40
|
+
*
|
|
41
|
+
* A home nunca e' "o projeto dele", tenha ela manifesto ou nao.
|
|
42
|
+
*/
|
|
43
|
+
const home = homedir();
|
|
44
|
+
let at = dir;
|
|
45
|
+
for (;;) {
|
|
46
|
+
if (at === home)
|
|
47
|
+
return null;
|
|
48
|
+
if (await exists(join(at, "package.json")))
|
|
49
|
+
return at;
|
|
50
|
+
/** A raiz do repositorio e' o teto: acima dela nao existe "o projeto dele". */
|
|
51
|
+
if (await exists(join(at, ".git")))
|
|
52
|
+
return null;
|
|
53
|
+
const up = dirname(at);
|
|
54
|
+
if (up === at)
|
|
55
|
+
return null;
|
|
56
|
+
at = up;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function exists(path) {
|
|
60
|
+
return stat(path).then(() => true, () => false);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A RECUSA, COM O MOTIVO E O QUE FAZER - nunca so' "nao posso".
|
|
64
|
+
*
|
|
65
|
+
* E ELA DIZ O QUE MEDIMOS, nao o que a pasta e'. A primeira redacao decretava
|
|
66
|
+
* *"<dir> is not a project"* e oferecia `npm init -y`: num repositorio Deno ou Rust a primeira
|
|
67
|
+
* frase e' falsa - aquilo E' um projeto - e a segunda manda sujar o repositorio dele com o
|
|
68
|
+
* manifesto de um ecossistema que ele nao usa. Representar o limite do leitor e' diferente de
|
|
69
|
+
* negar o que ele tem.
|
|
70
|
+
*/
|
|
71
|
+
export function notAProject(command, dir) {
|
|
72
|
+
return [
|
|
73
|
+
`No package.json in ${dir}, or in any folder above it up to the repository root - so this reader cannot tell which project it is looking at.`,
|
|
74
|
+
`\`${command}\` writes agent files into a project - a hook, an MCP entry, CLAUDE.md - so it stops before writing anything.`,
|
|
75
|
+
`Point it at the project: npx synthesisui ${command} --dir <path/to/project>`,
|
|
76
|
+
`(it looks for a package.json - that is what this reader knows how to place files in)`,
|
|
77
|
+
];
|
|
78
|
+
}
|
package/dist/sheet-needed.js
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { TAILWIND_THEME_VARS } from "./tailwind-theme-vars.js";
|
|
4
|
+
/**
|
|
5
|
+
* AS CLASSES QUE A FOLHA COMPILADA DECLARA COMO RECEITA - o nome, não o prefixo.
|
|
6
|
+
*
|
|
7
|
+
* A convenção viaja com o sistema (`ds-`, `sui-`, o que o projeto dele usa), então perguntar pelo
|
|
8
|
+
* prefixo seria fixar a forma de um repositório. O que é estrutural é onde a regra MORA: o
|
|
9
|
+
* compilador emite uma regra por receita dentro de `@layer components`, e é só essa camada que a
|
|
10
|
+
* folha traz e o projeto dele não tem.
|
|
11
|
+
*
|
|
12
|
+
* A PRIMEIRA VERSÃO VARRIA O TEXTO INTEIRO, e a revisão do fecho mediu o preço na folha viva do
|
|
13
|
+
* `codelevel` (143.409 bytes, versão instalada): **147 nomes extraídos, 3 deles não são receita** -
|
|
14
|
+
* `dark`, que é a classe DELE em `@layer base`; `layer-3d`, um utilitário DELE espelhado em
|
|
15
|
+
* `@layer utilities`; e `background`, que aparecia como VALOR de uma declaração e nem seletor era.
|
|
16
|
+
* Rodada sobre o repositório dele, a função devolvia `["layer-3d", "dark"]` - duas classes DELE
|
|
17
|
+
* sustentando a cobrança da NOSSA folha. É o defeito de 07/09 voltando pela porta do balde novo, e
|
|
18
|
+
* o oposto exato do que esta medição existe para permitir.
|
|
19
|
+
*
|
|
20
|
+
* O ESCAPE CONTA como parte do nome, porque no CSS ele é como se escreve um caractere que o
|
|
21
|
+
* seletor não aceita cru: `.ds-w-\[10px\]` é a classe `ds-w-[10px]`, e `.sm\:ds-hero` é a variante
|
|
22
|
+
* `sm:` da classe `ds-hero` - que é como o código dele a escreve. Truncar no `\` produziria os
|
|
23
|
+
* nomes `ds-w-` e `sm`, que ninguém escreve: uma dependência real deixaria de ser contada, e esse é
|
|
24
|
+
* o único lado do erro que o cabeçalho deste módulo declara inaceitável.
|
|
25
|
+
*/
|
|
26
|
+
export function classesTheSheetDeclares(css) {
|
|
27
|
+
const out = new Set();
|
|
28
|
+
for (const body of layerBodies(css, "components"))
|
|
29
|
+
for (const prelude of selectorPreludes(body))
|
|
30
|
+
for (const m of prelude.matchAll(/\.((?:\\.|[\w-])+)/g)) {
|
|
31
|
+
const bare = m[1].replace(/\\(.)/g, "$1");
|
|
32
|
+
/** A MESMA normalização que `classesIn` aplica ao que ele ESCREVE - `sm:ds-hero` é `ds-hero`. */
|
|
33
|
+
out.add((bare.split(":").pop() ?? bare).replace(/^[!-]/, ""));
|
|
34
|
+
}
|
|
35
|
+
return [...out];
|
|
36
|
+
}
|
|
37
|
+
/** O corpo de cada `@layer <nome> { … }`, com as chaves de dentro contadas. */
|
|
38
|
+
function layerBodies(css, name) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const m of css.matchAll(new RegExp(`@layer\\s+${name}\\s*\\{`, "g"))) {
|
|
41
|
+
let depth = 1;
|
|
42
|
+
let i = (m.index ?? 0) + m[0].length;
|
|
43
|
+
const start = i;
|
|
44
|
+
for (; i < css.length && depth > 0; i += 1) {
|
|
45
|
+
const c = css[i];
|
|
46
|
+
if (c === "{")
|
|
47
|
+
depth += 1;
|
|
48
|
+
else if (c === "}")
|
|
49
|
+
depth -= 1;
|
|
50
|
+
}
|
|
51
|
+
out.push(css.slice(start, i - 1));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* O PRELÚDIO DE CADA REGRA - o texto entre o fim da regra anterior e a `{` desta.
|
|
57
|
+
*
|
|
58
|
+
* É o que separa um SELETOR de um valor de declaração: `background: p.background` mora depois da
|
|
59
|
+
* `{`, e nunca chega aqui. Um prelúdio que começa com `@` é uma regra de agrupamento (`@media`,
|
|
60
|
+
* `@supports`) e não declara classe nenhuma - o corpo dela é varrido pela mesma volta do laço.
|
|
61
|
+
*/
|
|
62
|
+
function selectorPreludes(body) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const parts = body.split("{");
|
|
65
|
+
for (const part of parts.slice(0, -1)) {
|
|
66
|
+
const tail = part.slice(part.lastIndexOf("}") + 1).trim();
|
|
67
|
+
if (tail && !tail.startsWith("@"))
|
|
68
|
+
out.push(tail);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
4
72
|
/** `--animate-shimmer` no `@theme` -> o nome `shimmer`, que é o que uma classe carrega. */
|
|
5
73
|
function themeNames(themeCss) {
|
|
6
74
|
const out = new Set();
|
|
@@ -207,14 +275,33 @@ export function whatOnlyTheSheetResolves(input) {
|
|
|
207
275
|
const classes = [
|
|
208
276
|
...new Set([...written].filter((c) => names.some((n) => c === n || c.endsWith(`-${n}`)))),
|
|
209
277
|
];
|
|
278
|
+
/** As classes que a folha declara e que ESTE texto veste - ver `recipeClasses`. */
|
|
279
|
+
const declared = new Set(classesTheSheetDeclares(input.sheetCss));
|
|
280
|
+
const recipeClasses = [
|
|
281
|
+
...new Set([...written].filter((c) => declared.has(c))),
|
|
282
|
+
];
|
|
210
283
|
return {
|
|
211
284
|
variables,
|
|
212
285
|
classes,
|
|
213
|
-
|
|
286
|
+
recipeClasses,
|
|
287
|
+
needed: variables.length > 0 || classes.length > 0 || recipeClasses.length > 0,
|
|
214
288
|
};
|
|
215
289
|
}
|
|
290
|
+
/** A folha compilada da versão instalada - as receitas moram nela, não no `theme.css`. */
|
|
291
|
+
export async function installedTokensCss(root, slug) {
|
|
292
|
+
return installedSheet(root, slug, "tokens.css");
|
|
293
|
+
}
|
|
216
294
|
/** O `theme.css` da versão instalada - a folha da raiz é um re-export de uma linha. */
|
|
217
295
|
export async function installedThemeCss(root, slug) {
|
|
296
|
+
return installedSheet(root, slug, "theme.css");
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* UM ARQUIVO DA VERSÃO PINADA - o `.lock` diz qual é, e a folha da raiz é um re-export de uma linha.
|
|
300
|
+
*
|
|
301
|
+
* Uma leitura só para as duas folhas: duas cópias da mesma decisão de versão é a forma de uma delas
|
|
302
|
+
* ficar atrás no dia em que o formato do `.lock` mudar.
|
|
303
|
+
*/
|
|
304
|
+
async function installedSheet(root, slug, filename) {
|
|
218
305
|
const dir = join(root, "_synthesisui", "ds", slug);
|
|
219
306
|
const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
|
|
220
307
|
let version = 0;
|
|
@@ -226,5 +313,5 @@ export async function installedThemeCss(root, slug) {
|
|
|
226
313
|
version = 0;
|
|
227
314
|
}
|
|
228
315
|
}
|
|
229
|
-
return readFile(join(dir, `v${version}`,
|
|
316
|
+
return readFile(join(dir, `v${version}`, filename), "utf8").catch(() => "");
|
|
230
317
|
}
|
package/dist/their-tongue.js
CHANGED
|
@@ -50,6 +50,33 @@ export function inTheirTongue(css, tongue, destination = "stylesheet") {
|
|
|
50
50
|
});
|
|
51
51
|
return { css: out, named, inlined, left: [...left].sort() };
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* O VOCABULARIO DESTE REPOSITORIO **SEM O `add` TER RODADO** - a folha deixa de ser pre-requisito.
|
|
55
|
+
*
|
|
56
|
+
* O QUE ISTO MUDA PARA O CLIENTE: ele pede UM componente e recebe um arquivo que ja' fala a
|
|
57
|
+
* lingua do codigo dele - `var(--color-ink-900)`, o nome que ELE deu - sem antes materializar as
|
|
58
|
+
* 1813 linhas, 111 variaveis e 628 referencias que o `add` escreve no repositorio (medido em
|
|
59
|
+
* 10/09). A nossa folha e' a REFERENCIA que o agente dele le'; exigi-la para trazer um botao
|
|
60
|
+
* invertia isso, e contrariava a definicao de produto: nada nosso no repositorio dele.
|
|
61
|
+
*
|
|
62
|
+
* A MEDICAO E' A MESMA DO `add`, so' que em memoria: `pointTokensAtTheirNames` varre as folhas
|
|
63
|
+
* DELE e o build dele, e responde quais das nossas variaveis tem nome dele. A diferenca e' que
|
|
64
|
+
* nada disso vai para o disco - nem o mapa, nem a folha.
|
|
65
|
+
*
|
|
66
|
+
* `null` quando o repositorio dele nao nomeia nada do que este sistema declara. Ai' a folha
|
|
67
|
+
* continua sendo o caminho, e quem chama DIZ isso - em vez de escrever `var(--ds-*)` cru e
|
|
68
|
+
* deixar a pessoa descobrir por que a cor nao apareceu.
|
|
69
|
+
*/
|
|
70
|
+
export async function tongueFromArtifacts(root, artifacts) {
|
|
71
|
+
const tokens = artifacts["tokens.css"] ?? "";
|
|
72
|
+
if (!tokens)
|
|
73
|
+
return null;
|
|
74
|
+
const { pointTokensAtTheirNames } = await import("./their-vars.js");
|
|
75
|
+
const { pairs } = await pointTokensAtTheirNames(root, { artifacts });
|
|
76
|
+
if (pairs.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
return tongueOf(pairs, tokens);
|
|
79
|
+
}
|
|
53
80
|
/**
|
|
54
81
|
* O VOCABULÁRIO DESTE REPOSITÓRIO, montado do que o `add` já deixou na pasta.
|
|
55
82
|
*
|
package/dist/wiring-read.js
CHANGED
|
@@ -36,6 +36,25 @@ import { detectAppDirs, sheetChainOf } from "./global-sheet.js";
|
|
|
36
36
|
*
|
|
37
37
|
* The system itself was always resolved from the root. So is this now.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* O QUE NOS ESCREVEMOS NAO E' FIACAO DELE - e isto era um FALSO SILENCIO, o pior dos dois.
|
|
41
|
+
*
|
|
42
|
+
* MEDIDO EM 10/09: o cabecalho que `component-codegen.ts` poe em todo `.tsx` materializado diz,
|
|
43
|
+
* em comentario, *"Global setup (once per app): import _synthesisui/ds/<slug>/tokens.css"* e
|
|
44
|
+
* *"put data-ds=<slug> on a root element"*. A varredura abaixo procura exatamente essas duas
|
|
45
|
+
* strings em QUALQUER arquivo do repositorio - entao, a partir do primeiro componente que nos
|
|
46
|
+
* mesmos escrevemos, ela passava a responder `imported: true` e `scoped: true` sobre um projeto
|
|
47
|
+
* que nao importa nada e nao tem escopo nenhum.
|
|
48
|
+
*
|
|
49
|
+
* O QUE O CLIENTE VIVIA: ele traz um componente, a tela diz *"This project already imports
|
|
50
|
+
* <slug>'s tokens.css and carries data-ds - nothing to set up"*, ele cola o componente na pagina,
|
|
51
|
+
* e ve' um bloco sem cor, sem tipografia e sem sombra. O comando acabou de garantir que estava
|
|
52
|
+
* tudo certo. O `doctor` le' pela mesma porta, entao dizia o mesmo.
|
|
53
|
+
*
|
|
54
|
+
* A instrucao que ESTE arquivo carrega e' uma instrucao, nao um cumprimento dela. Um comentario
|
|
55
|
+
* nosso nunca prova nada sobre o projeto dele.
|
|
56
|
+
*/
|
|
57
|
+
const OURS = "Generated by SynthesisUI";
|
|
39
58
|
export async function readWiring(root, slug) {
|
|
40
59
|
const w = {
|
|
41
60
|
imported: false,
|
|
@@ -84,6 +103,9 @@ export async function readWiring(root, slug) {
|
|
|
84
103
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
85
104
|
if (!src)
|
|
86
105
|
continue;
|
|
106
|
+
/** Ver `OURS`: o nosso proprio cabecalho respondia por ele. */
|
|
107
|
+
if (src.slice(0, 300).includes(OURS))
|
|
108
|
+
continue;
|
|
87
109
|
if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
|
|
88
110
|
w.imported = true;
|
|
89
111
|
if (src.includes(`_synthesisui/ds/${slug}/theme.css`))
|
package/dist/written.js
CHANGED
|
@@ -60,11 +60,33 @@ export async function editedSinceWritten(recorded, dir) {
|
|
|
60
60
|
return edited;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* A
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* A PERGUNTA, NUM LUGAR SO': *ele editou o que escrevemos aqui?*
|
|
64
|
+
*
|
|
65
|
+
* As duas linhas - ler o registro do `.lock`, comparar com o disco - estavam copiadas em cada
|
|
66
|
+
* comando que se lembrou de perguntar, e ausentes nos que nao se lembraram. Medido em 10/09:
|
|
67
|
+
* `component` 6 guardas, `upgrade` 4, `generate` e `refit` gravavam o fingerprint e nunca o liam,
|
|
68
|
+
* `template` nao fazia nem uma coisa nem outra.
|
|
69
|
+
*
|
|
70
|
+
* Uma porta so' torna a omissao visivel: um comando de materializacao que nao a chama aparece no
|
|
71
|
+
* portao de `every-writer-keeps-his-edits.spec.ts`, em vez de aparecer no repositorio de alguem.
|
|
72
|
+
*
|
|
73
|
+
* `null` e' install legado (sem fingerprint gravado) - sem memoria nao ha' veredito, e quem chama
|
|
74
|
+
* mantem o comportamento de sempre.
|
|
75
|
+
*/
|
|
76
|
+
export async function editedHere(slugDir, entry, dir) {
|
|
77
|
+
return editedSinceWritten((await readWritten(slugDir))[entry], dir);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A FRASE DO TERMINAL, uma por arquivo mantido - o critério do T7 é a prova VISÍVEL: o dono
|
|
81
|
+
* fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato.
|
|
82
|
+
*
|
|
83
|
+
* `retake` E' OBRIGATORIO, e é o comando que RETOMA a versão nova. Ele era fixo em
|
|
84
|
+
* `component <slug> <entry>`, o que estava certo enquanto só um comando falava esta frase: no
|
|
85
|
+
* `template` aquela linha mandaria a pessoa rodar o comando errado, e uma saída que não funciona
|
|
86
|
+
* é pior que nenhuma. Obrigatório em vez de opcional porque um default aqui volta a ser o
|
|
87
|
+
* comando de outro comando, calado.
|
|
66
88
|
*/
|
|
67
|
-
export function keptLine(
|
|
89
|
+
export function keptLine(entry, edited, retake) {
|
|
68
90
|
return (`! ${entry}: you edited ${edited.join(", ")} after we wrote it - kept YOUR file(s).\n` +
|
|
69
|
-
` To take the new version anyway:
|
|
91
|
+
` To take the new version anyway: ${retake}`);
|
|
70
92
|
}
|
package/package.json
CHANGED