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.
@@ -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
  *
@@ -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"
@@ -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
- return hits.length === 1 ? hits[0] : null;
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
- const theirColorKey = (value, themeVars) => {
634
- const key = colorKey(value);
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
- lines.push(plain("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."));
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
+ }
@@ -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
- export const MATERIALISER_SINCE = "0.16.462";
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
- export const CHECKER_SINCE = "0.16.458";
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(`No design system published with slug "${slug}"${version != null ? ` at version v${version}` : ""}. Run \`synthesisui list\` to see what's available.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.462",
3
+ "version": "0.16.464",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {