synthesisui 0.16.462 → 0.16.464
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/changed-files.js +62 -1
- package/dist/commands/connect.js +4 -0
- package/dist/commands/hook.js +11 -0
- package/dist/component-codegen.js +49 -8
- package/dist/guarantee.js +23 -3
- package/dist/install-marks.js +19 -2
- package/dist/registry.js +7 -1
- package/package.json +1 -1
package/dist/changed-files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { stat } from "node:fs/promises";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
const run = promisify(execFile);
|
|
@@ -25,8 +25,69 @@ async function listed(root) {
|
|
|
25
25
|
if (line.trim())
|
|
26
26
|
out.push(line.trim());
|
|
27
27
|
}
|
|
28
|
+
if (out.length === 0 && !(await isGitRepo(root)))
|
|
29
|
+
return walked(root);
|
|
28
30
|
return [...new Set(out)];
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* FORA DO GIT, A PASTA É A LISTA - rodada 3 do refinamento, 27/09. O leigo-3 nasceu sem repositório, e
|
|
34
|
+
* as duas perguntas ao git voltavam vazias: o fim do turno não checava arquivo nenhum, nunca - o ledger
|
|
35
|
+
* sem uma linha `hook`, o envio com `count: 0`, o dashboard inteiro do agente sem checagem. Sem git, a
|
|
36
|
+
* resposta é andar pela pasta; o filtro pelo relógio do turno (em `changedSince`) continua valendo.
|
|
37
|
+
*/
|
|
38
|
+
const WALK_SKIP = new Set(["node_modules", ".next", "dist", "build", "out", "_synthesisui", "coverage"]);
|
|
39
|
+
const WALK_MAX = 5000;
|
|
40
|
+
async function isGitRepo(root) {
|
|
41
|
+
const { stdout } = await run("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root }).catch(() => ({
|
|
42
|
+
stdout: "",
|
|
43
|
+
}));
|
|
44
|
+
return stdout.trim() === "true";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* O `.gitignore` DA RAIZ, NA FORMA SIMPLES - rodada 4 do refinamento, 27/09. No leigo-4 (sem git) o fim
|
|
48
|
+
* do turno checou o `next-env.d.ts`, que o Next gera e que o `.gitignore` do `create-next-app` já exclui:
|
|
49
|
+
* sem git, ninguém lia aquele arquivo. Cobre nome, `/` na frente (só na raiz), `/` no fim (só pasta) e
|
|
50
|
+
* `*`; a negação (`!`) é ignorada - no pior caso um arquivo que ele quer checado fica de fora, como era
|
|
51
|
+
* antes do conserto de 27/09 para a pasta inteira.
|
|
52
|
+
*/
|
|
53
|
+
async function ignoredBy(root) {
|
|
54
|
+
const text = await readFile(join(root, ".gitignore"), "utf8").catch(() => "");
|
|
55
|
+
const rules = text
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((l) => l.trim())
|
|
58
|
+
.filter((l) => l && !l.startsWith("#") && !l.startsWith("!"))
|
|
59
|
+
.map((l) => {
|
|
60
|
+
const onlyDir = l.endsWith("/");
|
|
61
|
+
const body = l.replace(/^\//, "").replace(/\/$/, "");
|
|
62
|
+
const anchored = l.startsWith("/") || body.includes("/");
|
|
63
|
+
const glob = body.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
64
|
+
return { onlyDir, re: new RegExp(anchored ? `^${glob}$` : `(^|/)${glob}$`) };
|
|
65
|
+
});
|
|
66
|
+
return (rel, dir) => rules.some((r) => (!r.onlyDir || dir) && r.re.test(rel));
|
|
67
|
+
}
|
|
68
|
+
async function walked(root) {
|
|
69
|
+
const out = [];
|
|
70
|
+
const ignored = await ignoredBy(root);
|
|
71
|
+
const go = async (rel) => {
|
|
72
|
+
if (out.length >= WALK_MAX)
|
|
73
|
+
return;
|
|
74
|
+
for (const entry of await readdir(join(root, rel), { withFileTypes: true }).catch(() => [])) {
|
|
75
|
+
if (entry.name.startsWith(".") || WALK_SKIP.has(entry.name))
|
|
76
|
+
continue;
|
|
77
|
+
const next = rel ? `${rel}/${entry.name}` : entry.name;
|
|
78
|
+
if (ignored(next, entry.isDirectory()))
|
|
79
|
+
continue;
|
|
80
|
+
if (entry.isDirectory())
|
|
81
|
+
await go(next);
|
|
82
|
+
else if (entry.isFile())
|
|
83
|
+
out.push(next);
|
|
84
|
+
if (out.length >= WALK_MAX)
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
await go("");
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
30
91
|
/**
|
|
31
92
|
* OS ARQUIVOS INTERESSANTES QUE MUDARAM DESDE `since`, do mais recente para o mais antigo.
|
|
32
93
|
*
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { armTurnClock } from "./hook.js";
|
|
1
2
|
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { codexPinBefore, exists, wireAgent } from "../agent-wiring.js";
|
|
@@ -477,6 +478,9 @@ export async function connect(opts) {
|
|
|
477
478
|
? say("✓ what the check records is sent to your dashboard at the end of each turn - your system changes only from a commit on a clean main")
|
|
478
479
|
: say("· what the check records stays on this machine - run `npx synthesisui sync` to send it"));
|
|
479
480
|
}
|
|
481
|
+
/** O relógio do turno nasce aqui - ver `armTurnClock`: o primeiro turno depois do `connect` é checado. */
|
|
482
|
+
if (want.hook && wired.hook !== "skipped")
|
|
483
|
+
await armTurnClock(root);
|
|
480
484
|
if (want.hook && wired.hook !== "skipped") {
|
|
481
485
|
row(wired.hook !== "already there", (() => {
|
|
482
486
|
return wired.hook === "added"
|
package/dist/commands/hook.js
CHANGED
|
@@ -88,6 +88,17 @@ const startClock = (root) => writeFile(ledgerPath(root), "", { flag: "a" }).catc
|
|
|
88
88
|
const TURN_CLOCK = "_synthesisui/.turn-clock";
|
|
89
89
|
const turnClockOf = (root) => stat(join(root, TURN_CLOCK)).then((s) => s.mtimeMs, () => 0);
|
|
90
90
|
const moveTurnClock = (root) => writeFile(join(root, TURN_CLOCK), "", "utf8").catch(() => { });
|
|
91
|
+
/**
|
|
92
|
+
* O RELÓGIO NASCE NO `connect`, E NÃO NO PRIMEIRO TURNO - rodada 3 do refinamento, 27/09.
|
|
93
|
+
*
|
|
94
|
+
* Sem relógio, o primeiro fim de turno só o acertava e calava (ver `startClock`). Isso protegia o
|
|
95
|
+
* repositório com trabalho antigo em aberto, e cobrava um preço que ninguém tinha medido: num projeto
|
|
96
|
+
* NOVO o primeiro turno é o trabalho inteiro. O dashboard do leigo-3 - cinco arquivos - e as telas das
|
|
97
|
+
* rodadas 1 e 2 nunca foram checados: o `ledger.jsonl` tinha zero linhas `hook` e o envio marcava
|
|
98
|
+
* `count: 0`. Armado na instalação, o relógio deixa de fora o que veio antes do `connect` (a intenção
|
|
99
|
+
* de antes) e vê tudo o que veio depois. `flag: "wx"`: um relógio que já existe não recua.
|
|
100
|
+
*/
|
|
101
|
+
export const armTurnClock = (root) => writeFile(join(root, TURN_CLOCK), "", { encoding: "utf8", flag: "wx" }).catch(() => { });
|
|
91
102
|
/**
|
|
92
103
|
* QUANTOS ARQUIVOS UM COMANDO DE SHELL PODE FAZER O PRODUTO RELATAR DE UMA VEZ.
|
|
93
104
|
*
|
|
@@ -611,8 +611,11 @@ export class ThemeVocab extends Set {
|
|
|
611
611
|
* O nome que casa vale se o valor também casa - ou se um dos dois lados não tem valor conhecido, que
|
|
612
612
|
* é o comportamento de antes. Sem nome que case, o VALOR acha o nome, mas só quando exatamente uma
|
|
613
613
|
* variável dele o segura: duas seria adivinhar qual das duas ele quis dizer.
|
|
614
|
+
*
|
|
615
|
+
* `prefer` desempata, e só ele: quando o empate inclui a palavra da própria propriedade (`border`
|
|
616
|
+
* numa declaração de borda), é esse o nome. Sem ela no empate, continua sem adivinhar (rodada 4).
|
|
614
617
|
*/
|
|
615
|
-
nameFor(ref, ns, byName) {
|
|
618
|
+
nameFor(ref, ns, byName, prefer = null) {
|
|
616
619
|
const path = /^\{([^}]+)\}$/.exec(ref.trim())?.[1];
|
|
617
620
|
const want = path ? this.dsValues.get(`--ds-${path.replace(/\./g, "-")}`) : undefined;
|
|
618
621
|
if (byName && this.has(`--${ns}-${byName}`)) {
|
|
@@ -625,17 +628,55 @@ export class ThemeVocab extends Set {
|
|
|
625
628
|
const hits = [...this.theirValues]
|
|
626
629
|
.filter(([n, v]) => n.startsWith(`--${ns}-`) && sameValue(v, want))
|
|
627
630
|
.map(([n]) => n.slice(ns.length + 3));
|
|
628
|
-
|
|
631
|
+
if (hits.length === 1)
|
|
632
|
+
return hits[0];
|
|
633
|
+
return prefer && hits.includes(prefer) ? prefer : null;
|
|
629
634
|
}
|
|
630
635
|
}
|
|
631
636
|
const sameValue = (a, b) => a.trim().toLowerCase().replace(/\s+/g, " ") === b.trim().toLowerCase().replace(/\s+/g, " ");
|
|
632
637
|
/** A chave de cor que ele escreve: pelo `ThemeVocab` quando há valores, pelo nome quando não. */
|
|
633
|
-
|
|
634
|
-
|
|
638
|
+
/**
|
|
639
|
+
* O DEGRAU DA PALETA PELO NOME - `{color.coral.700}` -> `coral-700` - rodada 3 do refinamento, 27/09.
|
|
640
|
+
*
|
|
641
|
+
* O badge do Zephyr pede degraus (`{color.coral.700}`, `{color.teal.700}`…), o projeto do leigo-3 declara
|
|
642
|
+
* `--color-coral-700` com o mesmo valor, e o badge saía com `#c2410c` cru: pelo nome só os papéis
|
|
643
|
+
* semânticos eram traduzidos, e pelo valor só quando UMA variável segura aquele valor - ali duas seguravam.
|
|
644
|
+
* Só vale pelo `ThemeVocab`, que confere o valor.
|
|
645
|
+
*/
|
|
646
|
+
const paletteKey = (v) => {
|
|
647
|
+
const m = /^\{color\.([a-z0-9-]+)\.([a-z0-9-]+)\}$/.exec(v.trim());
|
|
648
|
+
if (!m || m[1] === "semantic" || m[1] === "series")
|
|
649
|
+
return null;
|
|
650
|
+
return `${m[1]}-${m[2]}`;
|
|
651
|
+
};
|
|
652
|
+
const theirColorKey = (value, themeVars, prefer = null) => {
|
|
653
|
+
const key = colorKey(value) ?? (themeVars instanceof ThemeVocab ? paletteKey(value) : null);
|
|
635
654
|
if (themeVars instanceof ThemeVocab)
|
|
636
|
-
return themeVars.nameFor(value, "color", key);
|
|
655
|
+
return themeVars.nameFor(value, "color", key, prefer);
|
|
637
656
|
return key && minted(themeVars, `--color-${key}`) ? key : null;
|
|
638
657
|
};
|
|
658
|
+
/** A palavra da propriedade que desempata dois nomes dele com o mesmo valor - ver `nameFor`. */
|
|
659
|
+
const preferFor = (prop) => (prop.startsWith("border") ? "border" : null);
|
|
660
|
+
/**
|
|
661
|
+
* AS CORES DE DENTRO DE UM VALOR COMPOSTO, NO NOME DELE - rodada 4 do refinamento, 27/09.
|
|
662
|
+
*
|
|
663
|
+
* O gradiente do botão do Gossamer (`linear-gradient(135deg, {color.semantic.accent}, …)`) não tem
|
|
664
|
+
* utilitário e ia inteiro para o arbitrário, onde a cor só achava o nome dele depois de um build: num
|
|
665
|
+
* Next novo saía `#eaa83c` com `--color-accent` declarado. Cada referência de cor é resolvida aqui pela
|
|
666
|
+
* mesma regra do utilitário (`theirColorKey`, que não precisa de build) e vira `var(--color-<nome>)`; a
|
|
667
|
+
* que não tem nome segue para a porta de tradução, que escreve o valor.
|
|
668
|
+
*
|
|
669
|
+
* `null` é "ninguém mediu", e aí nada é trocado: escrever `var(--color-x)` sem saber se ele declara
|
|
670
|
+
* seria uma cor que some.
|
|
671
|
+
*/
|
|
672
|
+
const namedInside = (prop, value, themeVars) => {
|
|
673
|
+
if (themeVars === null)
|
|
674
|
+
return value;
|
|
675
|
+
return value.replace(/\{color\.[a-zA-Z0-9.-]+\}/g, (ref) => {
|
|
676
|
+
const key = theirColorKey(ref, themeVars, preferFor(prop));
|
|
677
|
+
return key ? `var(--color-${key})` : ref;
|
|
678
|
+
});
|
|
679
|
+
};
|
|
639
680
|
/** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
|
|
640
681
|
* otherwise - never dropped). */
|
|
641
682
|
function declToTailwind(prop, value, themeVars) {
|
|
@@ -658,7 +699,7 @@ function declToTailwind(prop, value, themeVars) {
|
|
|
658
699
|
break;
|
|
659
700
|
}
|
|
660
701
|
case "borderColor": {
|
|
661
|
-
const key = theirColorKey(value, themeVars);
|
|
702
|
+
const key = theirColorKey(value, themeVars, "border");
|
|
662
703
|
if (key)
|
|
663
704
|
return [`border-${key}`];
|
|
664
705
|
break;
|
|
@@ -667,7 +708,7 @@ function declToTailwind(prop, value, themeVars) {
|
|
|
667
708
|
// "1px solid {color.semantic.x}" → border + border-<x>
|
|
668
709
|
const m = value.match(/^1px\s+solid\s+(\{[^}]+\})$/);
|
|
669
710
|
if (m) {
|
|
670
|
-
const key = theirColorKey(m[1], themeVars);
|
|
711
|
+
const key = theirColorKey(m[1], themeVars, "border");
|
|
671
712
|
if (key)
|
|
672
713
|
return ["border", `border-${key}`];
|
|
673
714
|
}
|
|
@@ -753,7 +794,7 @@ function declToTailwind(prop, value, themeVars) {
|
|
|
753
794
|
break;
|
|
754
795
|
}
|
|
755
796
|
}
|
|
756
|
-
return [arbitrary(prop, value)];
|
|
797
|
+
return [arbitrary(prop, namedInside(prop, value, themeVars))];
|
|
757
798
|
}
|
|
758
799
|
/**
|
|
759
800
|
* TAILWIND'S OWN WORD FOR A STATE, WHEN IT HAS ONE - and a mechanical rule when it
|
package/dist/guarantee.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { hasHook } from "./agent-wiring.js";
|
|
4
4
|
import { ungovernedIn } from "./governed.js";
|
|
5
5
|
/**
|
|
@@ -104,7 +104,14 @@ system) {
|
|
|
104
104
|
* invisível: um arquivo novo nunca apagou nada, então a checagem cala com razão e a pessoa lê
|
|
105
105
|
* esse silêncio como aprovação.
|
|
106
106
|
*/
|
|
107
|
-
|
|
107
|
+
/**
|
|
108
|
+
* SEM GIT, A METADE DO "ANTES" NÃO EXISTE - rodada 3 do refinamento, 27/09. O leigo-3 nasceu sem
|
|
109
|
+
* repositório (o `create-next-app` dele não criou um), e a frase de baixo falava de "your last commit"
|
|
110
|
+
* numa pasta que não tem commit nenhum. Dizer é o que transforma o silêncio em um passo.
|
|
111
|
+
*/
|
|
112
|
+
lines.push(plain((await inGitRepo(root))
|
|
113
|
+
? "What a file no longer has is measured against your last commit. A file git has never seen has no before, so nothing is reported as removed there."
|
|
114
|
+
: "This folder is not in a git repository, so nothing can be measured against a last commit - what a file lost goes unseen. `git init` and a first commit turn that on."));
|
|
108
115
|
lines.push(system.utilities.size > 0
|
|
109
116
|
? {
|
|
110
117
|
text: "A value counts as coming from your system in two spellings: `var(--token)`, and the {n} utility names your own theme generates - `bg-primary`, `p-6`, `rounded-lg`.",
|
|
@@ -169,3 +176,16 @@ export async function guaranteeIfNew(root, system) {
|
|
|
169
176
|
await writeFile(join(root, SAID), `${answer.fingerprint}\n`, "utf8").catch(() => { });
|
|
170
177
|
return answer.lines;
|
|
171
178
|
}
|
|
179
|
+
/** Esta pasta, ou uma acima dela, tem `.git`? - um monorepo tem o `.git` na raiz, não no app. */
|
|
180
|
+
async function inGitRepo(root) {
|
|
181
|
+
let at = root;
|
|
182
|
+
for (let i = 0; i < 40; i += 1) {
|
|
183
|
+
if (await stat(join(at, ".git")).then(() => true, () => false))
|
|
184
|
+
return true;
|
|
185
|
+
const up = dirname(at);
|
|
186
|
+
if (up === at)
|
|
187
|
+
return false;
|
|
188
|
+
at = up;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
package/dist/install-marks.js
CHANGED
|
@@ -264,7 +264,18 @@
|
|
|
264
264
|
* animação, e os componentes com `as` passam a ter o tipo genérico - rodada 1 do refinamento. Uma pasta
|
|
265
265
|
* escrita antes ensina o agente a colar uma fonte que não carrega.
|
|
266
266
|
*/
|
|
267
|
-
|
|
267
|
+
/**
|
|
268
|
+
* 0.16.462 -> 0.16.463 em 27/09, e o passo 1 dá **SIM**: o gerador que o `upgrade` usa passa a achar pelo
|
|
269
|
+
* nome os degraus da paleta que o projeto declara (`{color.coral.700}` -> `bg-coral-700`) - o badge do
|
|
270
|
+
* Zephyr saía com quatro cores cruas (rodada 3 do refinamento).
|
|
271
|
+
*/
|
|
272
|
+
/**
|
|
273
|
+
* 0.16.463 -> 0.16.464 em 27/09, e o passo 1 dá **SIM**: o gerador que o `upgrade` usa passa a achar o nome
|
|
274
|
+
* dele dentro de um valor composto - o gradiente do botão do Gossamer saía `#eaa83c, #dd4161` com
|
|
275
|
+
* `--color-accent` e `--color-primary` declarados - e a desempatar pela palavra da propriedade numa borda
|
|
276
|
+
* (rodada 4 do refinamento).
|
|
277
|
+
*/
|
|
278
|
+
export const MATERIALISER_SINCE = "0.16.464";
|
|
268
279
|
/**
|
|
269
280
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
270
281
|
*
|
|
@@ -420,7 +431,13 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
|
|
|
420
431
|
* hand" no `@theme` que o leigo copiou do GUIDE). Um hook fixado antes continua acusando o vocabulário do
|
|
421
432
|
* projeto como valor à mão, em todo turno.
|
|
422
433
|
*/
|
|
423
|
-
|
|
434
|
+
/**
|
|
435
|
+
* 0.16.458 -> 0.16.463 em 27/09, e o passo 1 dá **SIM**: o fim do turno passa a checar o primeiro turno
|
|
436
|
+
* depois do `connect` (o relógio nasce na instalação) e a enxergar os arquivos de uma pasta sem git -
|
|
437
|
+
* rodada 3 do refinamento: o dashboard do leigo-3 inteiro nunca foi checado. Um hook anterior segue calado
|
|
438
|
+
* nos dois casos.
|
|
439
|
+
*/
|
|
440
|
+
export const CHECKER_SINCE = "0.16.463";
|
|
424
441
|
/**
|
|
425
442
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
426
443
|
*
|
package/dist/registry.js
CHANGED
|
@@ -53,7 +53,13 @@ export async function fetchDesignSystem(base, slug, version) {
|
|
|
53
53
|
url.searchParams.set("version", String(version));
|
|
54
54
|
const res = await request(url.toString());
|
|
55
55
|
if (res.status === 404) {
|
|
56
|
-
throw new RegistryError(
|
|
56
|
+
throw new RegistryError(
|
|
57
|
+
/**
|
|
58
|
+
* A CONTA É A PRIMEIRA SUSPEITA - rodada 3 do refinamento, 27/09. O fork `zephyr` do dono estava na
|
|
59
|
+
* conta do navegador, e o terminal, logado em outra: o registry só entrega um sistema que não é
|
|
60
|
+
* público à conta dona, e a frase dizia "no design system published". Ele parou ali.
|
|
61
|
+
*/
|
|
62
|
+
`No design system "${slug}"${version != null ? ` at version v${version}` : ""} that this machine can see. If it is a system in your account, this machine may be signed in to another one - run \`npx synthesisui@latest login\` with the account that owns it. Otherwise, \`synthesisui list\` shows what is public.`);
|
|
57
63
|
}
|
|
58
64
|
if (!res.ok) {
|
|
59
65
|
throw new RegistryError(`Registry responded ${res.status} while fetching "${slug}".`);
|
package/package.json
CHANGED