synthesisui 0.16.178 → 0.16.180

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.
@@ -25,23 +25,45 @@ import { detectStack } from "../stack.js";
25
25
  *
26
26
  * Escrito uma vez e nunca reescrito: um arquivo que alguém ajustou é decisão dele.
27
27
  */
28
+ /**
29
+ * O que NÃO é do repositório: a medição e o registro local. Ambos são reescritos por inteiro a cada
30
+ * rodada e conflitariam em todo merge; o `.ledger-sent` descreve este clone e mais nada.
31
+ */
32
+ const IGNORED = [
33
+ "census.json",
34
+ "ledger.jsonl",
35
+ ".ledger-sent",
36
+ "not-expressed.md",
37
+ ];
38
+ const IGNORE_HEADER = "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
39
+ "# clone is governed; the measurement and the local record are not, because\n" +
40
+ "# both are rewritten in full on every run and would conflict on every merge.\n";
28
41
  async function writeGovernanceIgnore(projectRoot) {
29
42
  const path = join(projectRoot, "_synthesisui", ".gitignore");
30
- try {
31
- await access(path);
43
+ const existing = await readFile(path, "utf8").catch(() => null);
44
+ /**
45
+ * O ARQUIVO DIZ "Managed by synthesisui", E ELE PRECISA SER.
46
+ *
47
+ * Isto saía na primeira linha quando o arquivo já existia, então ele era escrito UMA vez e nunca
48
+ * mais. A consequência aparece na primeira vez que a gente acrescenta um registro local: em 07/08
49
+ * nasceu o `.ledger-sent`, e todo repositório que já tinha o `.gitignore` passaria a commitar um
50
+ * cursor que descreve a máquina de UMA pessoa. Um arquivo que se declara gerenciado e não é
51
+ * reconciliado é uma promessa quebrada com o time de alguém.
52
+ *
53
+ * ACRESCENTA, NUNCA REMOVE: uma linha que a pessoa escreveu ali é decisão dela, e some no dia em
54
+ * que a gente reescrevesse o arquivo inteiro. E só grava quando falta algo - um comando que sempre
55
+ * produz diff é um comando que o time para de rodar.
56
+ */
57
+ if (existing == null) {
58
+ await writeFile(path, `${IGNORE_HEADER}${IGNORED.map((l) => `${l}\n`).join("")}`, "utf8");
32
59
  return;
33
60
  }
34
- catch {
35
- // não existe - escrever
36
- }
37
- await writeFile(path, "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
38
- "# clone is governed; the measurement and the local record are not, because\n" +
39
- "# both are rewritten in full on every run and would conflict on every merge.\n" +
40
- "census.json\n" +
41
- "ledger.jsonl\n" +
42
- /** O cursor de envio descreve ESTE clone, não o repositório - ver `markSent`. */
43
- ".ledger-sent\n" +
44
- "not-expressed.md\n", "utf8");
61
+ const lines = new Set(existing.split("\n").map((l) => l.trim()));
62
+ const missing = IGNORED.filter((l) => !lines.has(l));
63
+ if (missing.length === 0)
64
+ return;
65
+ const sep = existing.endsWith("\n") ? "" : "\n";
66
+ await writeFile(path, `${existing}${sep}${missing.map((l) => `${l}\n`).join("")}`, "utf8");
45
67
  }
46
68
  /**
47
69
  * APAGA A PROSA QUE ESTA VERSÃO DEIXOU DE ESCREVER.
@@ -143,13 +143,14 @@ opts = {}) {
143
143
  * de hoje escreve `doctrine.json` e o de ontem materializava dois `.md` -, e o `upgrade` sai sem
144
144
  * fazer nada quando não há gap de versão, então nenhum comando alcançava aquele estado.
145
145
  *
146
- * O `connect` conserta na hora (ver `refreshInstall`), e esta linha existe para quem ainda não
147
- * rodou: a verificação de abertura é o único lugar que fala sem ser perguntado.
146
+ * O `upgrade` conserta, e esta linha existe para quem ainda não rodou: a verificação de abertura é
147
+ * o único lugar que fala sem ser perguntado. Apontava `connect` até 07/08, quando o dono nomeou a
148
+ * incoerência - a palavra que significa atualizar era a única que não atualizava.
148
149
  */
149
150
  if (cli && lock.cli && lock.cli !== cli)
150
151
  out.push({
151
152
  says: `the files under _synthesisui/ds/${lock.slug} were written by CLI ${lock.cli} and you are running ${cli} - the design system's version has not changed, but what this CLI writes into that folder has.`,
152
- run: "npx synthesisui connect",
153
+ run: "npx synthesisui upgrade",
153
154
  });
154
155
  if (cli && lock.version == null)
155
156
  out.push({
@@ -208,7 +209,11 @@ export async function versionBehind(root, opts = {}) {
208
209
  body.compiler !== (lock.compiler ?? null))
209
210
  return {
210
211
  says: `the css for "${lock.slug}" v${lock.version} is compiled differently now - same version, same document, a fix on our side. The files in this repo were written before it.`,
211
- run: "npx synthesisui connect",
212
+ /**
213
+ * `upgrade` E NÃO `connect` - a palavra que significa atualizar passa a ser a que atualiza.
214
+ * Ver a redistribuição em `upgrade`: `connect` volta a ser a fiação, uma vez.
215
+ */
216
+ run: "npx synthesisui upgrade",
212
217
  };
213
218
  /**
214
219
  * AS REGRAS MUDARAM - e nem a versão nem o CLI diziam isso.
@@ -222,7 +227,7 @@ export async function versionBehind(root, opts = {}) {
222
227
  if (body.rulesStamp && body.rulesStamp !== (lock.rules ?? null))
223
228
  return {
224
229
  says: `the rules that govern "${lock.slug}" changed since this repo materialized them - your agent reads the copy on disk, so it is still following the previous set.`,
225
- run: "npx synthesisui connect",
230
+ run: "npx synthesisui upgrade",
226
231
  };
227
232
  return null;
228
233
  }
@@ -245,9 +250,11 @@ from = "session") {
245
250
  if (items.length === 0)
246
251
  return "";
247
252
  return [
248
- from === "shell"
249
- ? "This repo is out of alignment with the design system that governs it:"
250
- : "Before this session starts, this environment is out of alignment with the design system that governs it:",
253
+ from === "after"
254
+ ? "Still out of alignment:"
255
+ : from === "shell"
256
+ ? "This repo is out of alignment with the design system that governs it:"
257
+ : "Before this session starts, this environment is out of alignment with the design system that governs it:",
251
258
  ...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
252
259
  ].join("\n");
253
260
  }
@@ -258,8 +265,15 @@ from = "session") {
258
265
  * ser lido. E nunca falha: um ambiente que não dá para verificar não pode impedir alguém de
259
266
  * trabalhar.
260
267
  */
261
- export async function align(opts) {
262
- const root = resolve(opts.dir ?? process.cwd());
268
+ /**
269
+ * TUDO QUE ESTÁ FORA DE ALINHO, local e remoto - a lista, sem imprimir nada.
270
+ *
271
+ * Extraída para os comandos que TERMINAM poderem fechar dizendo o que ainda falta. Sem isso, cada um
272
+ * teria a própria versão da pergunta: o `status` já tinha, lendo `.lock`, ledger e requests por conta
273
+ * própria e sem saber de defasagem nenhuma - duas portas para a mesma sala, e uma delas cega
274
+ * (dono, 07/08).
275
+ */
276
+ export async function misalignments(root, opts = {}) {
263
277
  const items = await localMisalignments(root, {
264
278
  ...(opts.cli ? { cli: opts.cli } : {}),
265
279
  }).catch(() => []);
@@ -267,6 +281,27 @@ export async function align(opts) {
267
281
  const remote = await versionBehind(root).catch(() => null);
268
282
  if (remote)
269
283
  items.push(remote);
284
+ return items;
285
+ }
286
+ /**
287
+ * A CAUDA DE UM COMANDO QUE TERMINOU: o que ainda está fora, ou nada.
288
+ *
289
+ * Muda quando não falta nada, que é o caso normal e é o que a mantém legível. Ela existe porque
290
+ * `sync`, `connect` e `upgrade` deixavam a pessoa sem saber se tinha acabado - e a resposta exigia
291
+ * lembrar de um sexto comando (dono, 07/08).
292
+ */
293
+ export async function reportWhatIsLeft(root, opts = {}) {
294
+ const items = await misalignments(root, opts).catch(() => []);
295
+ if (items.length === 0)
296
+ return;
297
+ console.log("");
298
+ console.log(describeMisalignments(items, "after"));
299
+ }
300
+ export async function align(opts) {
301
+ const root = resolve(opts.dir ?? process.cwd());
302
+ const items = await misalignments(root, {
303
+ ...(opts.cli ? { cli: opts.cli } : {}),
304
+ });
270
305
  const text = describeMisalignments(items, opts.shell ? "shell" : "session");
271
306
  if (text)
272
307
  console.log(text);
@@ -8,6 +8,7 @@ import { hasHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell
8
8
  import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "../skill-import.js";
9
9
  import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
10
10
  import { add } from "./add.js";
11
+ import { reportWhatIsLeft } from "./align.js";
11
12
  import { ci } from "./ci.js";
12
13
  import { MCP_TOOL_COUNT } from "./mcp.js";
13
14
  /**
@@ -343,6 +344,7 @@ export async function connect(opts) {
343
344
  console.log(body('A project MCP server needs approving once; say yes when it asks. Then "/mcp" lists synthesisui.'));
344
345
  }
345
346
  }
347
+ await reportWhatIsLeft(root, { cli: opts.version });
346
348
  if (wired.command.startsWith("npx synthesisui@")) {
347
349
  console.log("");
348
350
  console.log(body("The hook runs through npx, which costs about 0.7s per edit. Adding"));
@@ -4,6 +4,7 @@ import { readToken, resolveRegistry } from "../config.js";
4
4
  import { readEvents } from "../doctor/ledger.js";
5
5
  import { readRequests } from "../doctor/requests.js";
6
6
  import { body, paint, section, snippet } from "../output.js";
7
+ import { misalignments } from "./align.js";
7
8
  /**
8
9
  * `synthesisui status` - a resposta do dia a dia, no terminal.
9
10
  *
@@ -143,7 +144,28 @@ export async function status(opts) {
143
144
  if (remote.publishedCli && opts.cli && remote.publishedCli !== opts.cli) {
144
145
  console.log("");
145
146
  console.log(body(`Your CLI is ${opts.cli} and ${remote.publishedCli} is published - a newer reader sees styles this one cannot.`));
146
- console.log(paint.blue(snippet(["npx synthesisui@latest connect"])));
147
+ console.log(paint.blue(snippet(["npx synthesisui@latest upgrade"])));
148
+ }
149
+ /**
150
+ * O QUE ESTÁ FORA DE ALINHO - e este comando era CEGO para isso.
151
+ *
152
+ * `status` nasceu antes do `align` e lia `.lock`, ledger e requests por conta própria: duas portas
153
+ * para a mesma sala, e a que a pessoa digita era a que não sabia de defasagem. Aqui ele passa a
154
+ * usar a mesma medição do gancho, então `align` deixa de ser algo que alguém precisa conhecer -
155
+ * ele continua existindo, chamado pelo `SessionStart` e pelo terminal (dono, 07/08).
156
+ */
157
+ const off = await misalignments(root, opts.cli ? { cli: opts.cli } : {});
158
+ console.log("");
159
+ if (off.length === 0) {
160
+ console.log(section("In step"));
161
+ console.log(body(paint.faint("Nothing here is behind the system that governs it - version, css, rules and the measurement all line up.")));
162
+ return;
163
+ }
164
+ console.log(section("Out of step"));
165
+ for (const item of off) {
166
+ console.log(body(item.says));
167
+ if (item.run)
168
+ console.log(paint.blue(snippet([item.run])));
147
169
  }
148
170
  }
149
171
  /** "2 hours ago", sem dependência - e `Invalid Date` nunca chega à tela. */
@@ -6,6 +6,7 @@ import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } fr
6
6
  import { measuredScope, rememberScope } from "../measured-scope.js";
7
7
  import { body, paint, section, snippet } from "../output.js";
8
8
  import { repoStateOf } from "../repo-state.js";
9
+ import { reportWhatIsLeft } from "./align.js";
9
10
  import { resolveReadParts, siblingProjects, takeCensus } from "./import.js";
10
11
  /**
11
12
  * What each decision means ON THIS MACHINE - the card decides, the sync
@@ -146,6 +147,15 @@ export async function sync(opts) {
146
147
  console.log("");
147
148
  console.log(body(`The record lives on your system's overview:`));
148
149
  console.log(body(` ${base}/dashboard/mine/${slug}`));
150
+ /**
151
+ * O QUE AINDA FALTA, no fim do comando que a pessoa acabou de rodar.
152
+ *
153
+ * `sync` e `align` respondem perguntas diferentes e as duas palavras se pareciam demais (dono,
154
+ * 07/08). Fundir era impossível - um escreve e o outro roda sozinho a cada sessão -, mas terminar
155
+ * dizendo o que sobrou resolve o que a dúvida realmente era: acabou ou não? Mudo quando não sobrou
156
+ * nada, que é o caso normal.
157
+ */
158
+ await reportWhatIsLeft(root);
149
159
  }
150
160
  /**
151
161
  * MEDE DE NOVO, AVISA, E ENVIA - o corpo do `sync` que faz um leitor novo chegar num sistema.
@@ -7,6 +7,7 @@ import { body, section, snippet } from "../output.js";
7
7
  import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
8
8
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
9
9
  import { add } from "./add.js";
10
+ import { reportWhatIsLeft } from "./align.js";
10
11
  import { doctor } from "./doctor.js";
11
12
  /**
12
13
  * The highest `v<n>` below `installed` among the folder names given, or the one
@@ -80,9 +81,54 @@ async function rewriteBrief(slug, slugDir, from, to) {
80
81
  * to `_synthesisui/ds/<slug>/UPGRADE.md` - the migration brief your agent
81
82
  * walks to update the app (breaking changes first).
82
83
  */
83
- export async function upgrade(slug, opts) {
84
+ /**
85
+ * O ÚNICO SISTEMA INSTALADO AQUI, quando ninguém nomeou um.
86
+ *
87
+ * O slug era obrigatório porque `upgrade` era um comando ocasional - só valia quando saía versão
88
+ * nova. Ao virar O comando de atualizar (ver abaixo), ele passa a ser rodado toda semana, e exigir um
89
+ * nome que a pessoa não decorou é atrito por nada. Com mais de um sistema o nome volta a ser
90
+ * necessário, e aí a mensagem lista os que existem. Mesma regra que o `sync` já segue.
91
+ */
92
+ /**
93
+ * O QUE ESTÁ DEFASADO NESTA INSTALAÇÃO, em uma frase - ou `null` quando nada está.
94
+ *
95
+ * As três formas de ficar para trás sem a versão andar. `?meta=1` custa algumas centenas de bytes, e
96
+ * falhar nele não pode custar o comando: sem resposta, a leitura honesta é "não sei", e não se
97
+ * rematerializa por suposição.
98
+ */
99
+ async function staleInstall(root, slug, version, base, cli) {
100
+ const lock = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8")
101
+ .then((raw) => JSON.parse(raw))
102
+ .catch(() => null);
103
+ if (!lock)
104
+ return null;
105
+ const res = await fetch(`${base}/api/registry/ds/${slug}?version=${version}&meta=1`).catch(() => null);
106
+ const meta = res?.ok
107
+ ? (await res.json().catch(() => null))
108
+ : null;
109
+ const why = [];
110
+ if (meta?.compiler != null && meta.compiler !== (lock.compiler ?? null))
111
+ why.push("the css is compiled differently now");
112
+ if (meta?.rulesStamp != null && meta.rulesStamp !== (lock.rules ?? null))
113
+ why.push("the rules that govern it changed");
114
+ if (cli && lock.cli && lock.cli !== cli)
115
+ why.push(`these files were written by CLI ${lock.cli}`);
116
+ return why.length > 0 ? `${why.join(", ")} - re-materializing` : null;
117
+ }
118
+ async function theOnlyInstalled(root) {
119
+ const dsDir = join(root, "_synthesisui", "ds");
120
+ const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
121
+ const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
122
+ if (slugs.length === 1)
123
+ return slugs[0];
124
+ if (slugs.length === 0)
125
+ throw new RegistryError("No design system is installed here - run `synthesisui add <slug>` first.");
126
+ throw new RegistryError(`More than one system is installed here (${slugs.join(", ")}) - name the one to update: \`synthesisui upgrade <slug>\`.`);
127
+ }
128
+ export async function upgrade(asked, opts) {
84
129
  const base = resolveRegistry(opts.registry);
85
130
  const root = opts.dir ?? process.cwd();
131
+ const slug = asked ?? (await theOnlyInstalled(root));
86
132
  const slugDir = join(root, "_synthesisui", "ds", slug);
87
133
  // installed version - upgrade only makes sense over an existing install
88
134
  let installed;
@@ -98,8 +144,33 @@ export async function upgrade(slug, opts) {
98
144
  console.log(`→ checking "${slug}" (installed: v${installed}) …`);
99
145
  const latest = await fetchDesignSystem(base, slug);
100
146
  if (latest.version === installed) {
147
+ /**
148
+ * MESMA VERSÃO NÃO SIGNIFICA EM DIA - e essa confusão era a nossa, não da pessoa.
149
+ *
150
+ * `upgrade` só agia sobre gap de VERSÃO. Mas o CSS é compilado a cada busca, as regras valem no
151
+ * instante em que são escritas, e os arquivos da pasta são escritos pelo CLI - três formas de
152
+ * ficar defasado sem a versão andar. Quem consertava isso era o `connect`, o que deixava a
153
+ * palavra que significa atualizar como a única que não atualizava (dono, 07/08).
154
+ *
155
+ * Então aqui ele rematerializa quando qualquer uma das três se moveu, e sai em silêncio quando
156
+ * nenhuma se moveu - que é o caso normal.
157
+ */
158
+ const drift = await staleInstall(root, slug, installed, base, opts.cli);
159
+ if (drift) {
160
+ console.log(`↻ ${slug} v${installed} - ${drift}`);
161
+ await add(slug, {
162
+ registry: opts.registry,
163
+ dir: root,
164
+ version: installed,
165
+ setupHints: false,
166
+ ...(opts.cli ? { cli: opts.cli } : {}),
167
+ });
168
+ await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
169
+ return;
170
+ }
101
171
  if (!opts.force) {
102
172
  console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
173
+ await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
103
174
  return;
104
175
  }
105
176
  // The brief is a PHOTOGRAPH: written once, at the moment of the upgrade,
@@ -262,5 +333,6 @@ export async function upgrade(slug, opts) {
262
333
  ]));
263
334
  console.log("");
264
335
  console.log(body(`(rollback: synthesisui add ${slug} --version ${installed})`));
336
+ await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
265
337
  console.log("");
266
338
  }
package/dist/index.js CHANGED
@@ -505,13 +505,12 @@ async function main() {
505
505
  break;
506
506
  }
507
507
  case "upgrade": {
508
- const slug = args[0];
509
- if (!slug) {
510
- console.error("error: provide the slug - `synthesisui upgrade <slug>`");
511
- process.exitCode = 1;
512
- return;
513
- }
514
- await upgrade(slug, {
508
+ /**
509
+ * O SLUG É OPCIONAL desde 07/08: `upgrade` deixou de ser ocasional e virou O comando de
510
+ * atualizar, então exigir um nome que a pessoa não decorou é atrito por nada. Com mais de um
511
+ * sistema instalado, `theOnlyInstalled` recusa e lista os que existem.
512
+ */
513
+ await upgrade(args[0], {
515
514
  registry,
516
515
  dir,
517
516
  cli: CLI_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.178",
3
+ "version": "0.16.180",
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": {