synthesisui 0.16.179 → 0.16.182

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,6 +1,7 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
+ import { pinnedHookVersion } from "../agent-wiring.js";
4
5
  import { readToken, resolveRegistry } from "../config.js";
5
6
  import { unsentEvents } from "../doctor/ledger.js";
6
7
  import { measuredScope } from "../measured-scope.js";
@@ -143,13 +144,31 @@ opts = {}) {
143
144
  * de hoje escreve `doctrine.json` e o de ontem materializava dois `.md` -, e o `upgrade` sai sem
144
145
  * fazer nada quando não há gap de versão, então nenhum comando alcançava aquele estado.
145
146
  *
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.
147
+ * O `upgrade` conserta, e esta linha existe para quem ainda não rodou: a verificação de abertura é
148
+ * o único lugar que fala sem ser perguntado. Apontava `connect` até 07/08, quando o dono nomeou a
149
+ * incoerência - a palavra que significa atualizar era a única que não atualizava.
148
150
  */
149
151
  if (cli && lock.cli && lock.cli !== cli)
150
152
  out.push({
151
153
  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",
154
+ run: "npx synthesisui upgrade",
155
+ });
156
+ /**
157
+ * O HOOK, MEDIDO NELE MESMO - e não pelo `.lock`, que é um proxy que se move sozinho.
158
+ *
159
+ * A linha acima compara o CLI que escreveu a PASTA. Desde que o `upgrade` passou a rematerializá-la
160
+ * (07/08), ela volta a bater sem que a fiação tenha sido refeita: o `.lock` dizia 0.16.180 e o
161
+ * `PostToolUse` continuava pinado em 0.16.178, com o `status` anunciando "In step". Um alarme
162
+ * desligado por um conserto parcial é pior que alarme nenhum.
163
+ *
164
+ * `pinnedHookVersion` devolve `null` numa instalação local, onde o hook segue o `node_modules` e
165
+ * nunca está atrás por si.
166
+ */
167
+ const pinned = await pinnedHookVersion(root).catch(() => null);
168
+ if (cli && pinned && pinned !== cli)
169
+ out.push({
170
+ says: `the check that runs after every write is pinned to CLI ${pinned} and you are running ${cli} - your agent's edits are being checked by an older reader than the one measuring this repo.`,
171
+ run: "npx synthesisui upgrade",
153
172
  });
154
173
  if (cli && lock.version == null)
155
174
  out.push({
@@ -208,7 +227,11 @@ export async function versionBehind(root, opts = {}) {
208
227
  body.compiler !== (lock.compiler ?? null))
209
228
  return {
210
229
  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",
230
+ /**
231
+ * `upgrade` E NÃO `connect` - a palavra que significa atualizar passa a ser a que atualiza.
232
+ * Ver a redistribuição em `upgrade`: `connect` volta a ser a fiação, uma vez.
233
+ */
234
+ run: "npx synthesisui upgrade",
212
235
  };
213
236
  /**
214
237
  * AS REGRAS MUDARAM - e nem a versão nem o CLI diziam isso.
@@ -222,7 +245,7 @@ export async function versionBehind(root, opts = {}) {
222
245
  if (body.rulesStamp && body.rulesStamp !== (lock.rules ?? null))
223
246
  return {
224
247
  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",
248
+ run: "npx synthesisui upgrade",
226
249
  };
227
250
  return null;
228
251
  }
@@ -245,9 +268,11 @@ from = "session") {
245
268
  if (items.length === 0)
246
269
  return "";
247
270
  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:",
271
+ from === "after"
272
+ ? "Still out of alignment:"
273
+ : from === "shell"
274
+ ? "This repo is out of alignment with the design system that governs it:"
275
+ : "Before this session starts, this environment is out of alignment with the design system that governs it:",
251
276
  ...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
252
277
  ].join("\n");
253
278
  }
@@ -258,8 +283,15 @@ from = "session") {
258
283
  * ser lido. E nunca falha: um ambiente que não dá para verificar não pode impedir alguém de
259
284
  * trabalhar.
260
285
  */
261
- export async function align(opts) {
262
- const root = resolve(opts.dir ?? process.cwd());
286
+ /**
287
+ * TUDO QUE ESTÁ FORA DE ALINHO, local e remoto - a lista, sem imprimir nada.
288
+ *
289
+ * Extraída para os comandos que TERMINAM poderem fechar dizendo o que ainda falta. Sem isso, cada um
290
+ * teria a própria versão da pergunta: o `status` já tinha, lendo `.lock`, ledger e requests por conta
291
+ * própria e sem saber de defasagem nenhuma - duas portas para a mesma sala, e uma delas cega
292
+ * (dono, 07/08).
293
+ */
294
+ export async function misalignments(root, opts = {}) {
263
295
  const items = await localMisalignments(root, {
264
296
  ...(opts.cli ? { cli: opts.cli } : {}),
265
297
  }).catch(() => []);
@@ -267,6 +299,27 @@ export async function align(opts) {
267
299
  const remote = await versionBehind(root).catch(() => null);
268
300
  if (remote)
269
301
  items.push(remote);
302
+ return items;
303
+ }
304
+ /**
305
+ * A CAUDA DE UM COMANDO QUE TERMINOU: o que ainda está fora, ou nada.
306
+ *
307
+ * Muda quando não falta nada, que é o caso normal e é o que a mantém legível. Ela existe porque
308
+ * `sync`, `connect` e `upgrade` deixavam a pessoa sem saber se tinha acabado - e a resposta exigia
309
+ * lembrar de um sexto comando (dono, 07/08).
310
+ */
311
+ export async function reportWhatIsLeft(root, opts = {}) {
312
+ const items = await misalignments(root, opts).catch(() => []);
313
+ if (items.length === 0)
314
+ return;
315
+ console.log("");
316
+ console.log(describeMisalignments(items, "after"));
317
+ }
318
+ export async function align(opts) {
319
+ const root = resolve(opts.dir ?? process.cwd());
320
+ const items = await misalignments(root, {
321
+ ...(opts.cli ? { cli: opts.cli } : {}),
322
+ });
270
323
  const text = describeMisalignments(items, opts.shell ? "shell" : "session");
271
324
  if (text)
272
325
  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
  *
@@ -139,11 +140,45 @@ export async function status(opts) {
139
140
  : `${owned.slug} on the platform did not come from an import, so there is no measurement to compare.`));
140
141
  if (remote.hasCensus)
141
142
  console.log(body(paint.dim("`synthesisui sync` measures this repo again with today's reader and sends it.")));
142
- /** E a distância até o leitor publicado, com o comando que a fecha - ver `sync`. */
143
- if (remote.publishedCli && opts.cli && remote.publishedCli !== opts.cli) {
144
- console.log("");
145
- 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"])));
143
+ /**
144
+ * A DISTÂNCIA ATÉ O LEITOR PUBLICADO - e ela desce para o veredito, em vez de ser um parágrafo solto.
145
+ *
146
+ * Isto imprimia acima e o veredito abaixo dizia "In step", no MESMO comando: duas seções
147
+ * discordando com três linhas de distância (dono, 07/08). Um comando que se contradiz não é meio
148
+ * certo, é inutilizável - quem lê passa a não acreditar em nenhuma das duas.
149
+ *
150
+ * Não vira um item do `align`: o CLI subiu seis vezes num dia e nenhuma delas mudou um leitor, e a
151
+ * verificação de abertura tem que ficar muda nesses casos. Aqui é diferente - a pessoa PERGUNTOU.
152
+ */
153
+ const behindCli = remote.publishedCli && opts.cli && remote.publishedCli !== opts.cli
154
+ ? {
155
+ says: `your CLI is ${opts.cli} and ${remote.publishedCli} is published - a newer reader sees styles this one cannot.`,
156
+ run: "npx synthesisui@latest upgrade",
157
+ }
158
+ : null;
159
+ /**
160
+ * O QUE ESTÁ FORA DE ALINHO - e este comando era CEGO para isso.
161
+ *
162
+ * `status` nasceu antes do `align` e lia `.lock`, ledger e requests por conta própria: duas portas
163
+ * para a mesma sala, e a que a pessoa digita era a que não sabia de defasagem. Aqui ele passa a
164
+ * usar a mesma medição do gancho, então `align` deixa de ser algo que alguém precisa conhecer -
165
+ * ele continua existindo, chamado pelo `SessionStart` e pelo terminal (dono, 07/08).
166
+ */
167
+ const off = [
168
+ ...(await misalignments(root, opts.cli ? { cli: opts.cli } : {})),
169
+ ...(behindCli ? [behindCli] : []),
170
+ ];
171
+ console.log("");
172
+ if (off.length === 0) {
173
+ console.log(section("In step"));
174
+ console.log(body(paint.faint("Nothing here is behind the system that governs it - version, css, rules, the measurement and this CLI all line up.")));
175
+ return;
176
+ }
177
+ console.log(section("Out of step"));
178
+ for (const item of off) {
179
+ console.log(body(item.says));
180
+ if (item.run)
181
+ console.log(paint.blue(snippet([item.run])));
147
182
  }
148
183
  }
149
184
  /** "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.
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { pinnedHookVersion, wireAgent } from "../agent-wiring.js";
3
4
  import { generateComponentFiles } from "../component-codegen.js";
4
5
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
6
  import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
@@ -7,6 +8,7 @@ import { body, section, snippet } from "../output.js";
7
8
  import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
8
9
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
9
10
  import { add } from "./add.js";
11
+ import { reportWhatIsLeft } from "./align.js";
10
12
  import { doctor } from "./doctor.js";
11
13
  /**
12
14
  * The highest `v<n>` below `installed` among the folder names given, or the one
@@ -80,9 +82,75 @@ async function rewriteBrief(slug, slugDir, from, to) {
80
82
  * to `_synthesisui/ds/<slug>/UPGRADE.md` - the migration brief your agent
81
83
  * walks to update the app (breaking changes first).
82
84
  */
83
- export async function upgrade(slug, opts) {
85
+ /**
86
+ * O ÚNICO SISTEMA INSTALADO AQUI, quando ninguém nomeou um.
87
+ *
88
+ * O slug era obrigatório porque `upgrade` era um comando ocasional - só valia quando saía versão
89
+ * nova. Ao virar O comando de atualizar (ver abaixo), ele passa a ser rodado toda semana, e exigir um
90
+ * nome que a pessoa não decorou é atrito por nada. Com mais de um sistema o nome volta a ser
91
+ * necessário, e aí a mensagem lista os que existem. Mesma regra que o `sync` já segue.
92
+ */
93
+ /**
94
+ * O QUE ESTÁ DEFASADO NESTA INSTALAÇÃO, em uma frase - ou `null` quando nada está.
95
+ *
96
+ * As três formas de ficar para trás sem a versão andar. `?meta=1` custa algumas centenas de bytes, e
97
+ * falhar nele não pode custar o comando: sem resposta, a leitura honesta é "não sei", e não se
98
+ * rematerializa por suposição.
99
+ */
100
+ async function staleInstall(root, slug, version, base, cli) {
101
+ const lock = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8")
102
+ .then((raw) => JSON.parse(raw))
103
+ .catch(() => null);
104
+ if (!lock)
105
+ return null;
106
+ const res = await fetch(`${base}/api/registry/ds/${slug}?version=${version}&meta=1`).catch(() => null);
107
+ const meta = res?.ok
108
+ ? (await res.json().catch(() => null))
109
+ : null;
110
+ const why = [];
111
+ if (meta?.compiler != null && meta.compiler !== (lock.compiler ?? null))
112
+ why.push("the css is compiled differently now");
113
+ if (meta?.rulesStamp != null && meta.rulesStamp !== (lock.rules ?? null))
114
+ why.push("the rules that govern it changed");
115
+ if (cli && lock.cli && lock.cli !== cli)
116
+ why.push(`these files were written by CLI ${lock.cli}`);
117
+ return why.length > 0 ? `${why.join(", ")} - re-materializing` : null;
118
+ }
119
+ /**
120
+ * A FIAÇÃO DO AGENTE, refeita quando ela ficou para trás.
121
+ *
122
+ * `upgrade` diz que traz para o dia tudo que está instalado aqui, e a pasta do DS é só metade: o hook
123
+ * `PostToolUse` é PINADO numa versão, então ele não anda sozinho. Em 07/08 o primeiro `upgrade` com o
124
+ * papel novo gravou `cli: 0.16.180` no `.lock` e deixou o hook em 0.16.178 - e como a verificação
125
+ * comparava o `.lock`, o `status` passou a dizer "In step" com a fiação duas versões atrás. Uma
126
+ * promessa pela metade que ainda por cima desliga o alarme.
127
+ *
128
+ * `wireAgent` é um merge idempotente: quando já está na versão certa, não escreve nada.
129
+ */
130
+ async function rewireIfBehind(root, cli) {
131
+ if (!cli)
132
+ return;
133
+ const pinned = await pinnedHookVersion(root).catch(() => null);
134
+ if (!pinned || pinned === cli)
135
+ return;
136
+ await wireAgent(root, cli, { hook: true, mcp: true }).catch(() => null);
137
+ console.log(`↻ the check after every write moved from ${pinned} to ${cli}`);
138
+ console.log(" Reopen your editor session - hooks are read at startup.");
139
+ }
140
+ async function theOnlyInstalled(root) {
141
+ const dsDir = join(root, "_synthesisui", "ds");
142
+ const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
143
+ const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
144
+ if (slugs.length === 1)
145
+ return slugs[0];
146
+ if (slugs.length === 0)
147
+ throw new RegistryError("No design system is installed here - run `synthesisui add <slug>` first.");
148
+ throw new RegistryError(`More than one system is installed here (${slugs.join(", ")}) - name the one to update: \`synthesisui upgrade <slug>\`.`);
149
+ }
150
+ export async function upgrade(asked, opts) {
84
151
  const base = resolveRegistry(opts.registry);
85
152
  const root = opts.dir ?? process.cwd();
153
+ const slug = asked ?? (await theOnlyInstalled(root));
86
154
  const slugDir = join(root, "_synthesisui", "ds", slug);
87
155
  // installed version - upgrade only makes sense over an existing install
88
156
  let installed;
@@ -98,8 +166,34 @@ export async function upgrade(slug, opts) {
98
166
  console.log(`→ checking "${slug}" (installed: v${installed}) …`);
99
167
  const latest = await fetchDesignSystem(base, slug);
100
168
  if (latest.version === installed) {
169
+ /**
170
+ * MESMA VERSÃO NÃO SIGNIFICA EM DIA - e essa confusão era a nossa, não da pessoa.
171
+ *
172
+ * `upgrade` só agia sobre gap de VERSÃO. Mas o CSS é compilado a cada busca, as regras valem no
173
+ * instante em que são escritas, e os arquivos da pasta são escritos pelo CLI - três formas de
174
+ * ficar defasado sem a versão andar. Quem consertava isso era o `connect`, o que deixava a
175
+ * palavra que significa atualizar como a única que não atualizava (dono, 07/08).
176
+ *
177
+ * Então aqui ele rematerializa quando qualquer uma das três se moveu, e sai em silêncio quando
178
+ * nenhuma se moveu - que é o caso normal.
179
+ */
180
+ const drift = await staleInstall(root, slug, installed, base, opts.cli);
181
+ if (drift) {
182
+ console.log(`↻ ${slug} v${installed} - ${drift}`);
183
+ await add(slug, {
184
+ registry: opts.registry,
185
+ dir: root,
186
+ version: installed,
187
+ setupHints: false,
188
+ ...(opts.cli ? { cli: opts.cli } : {}),
189
+ });
190
+ await rewireIfBehind(root, opts.cli);
191
+ await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
192
+ return;
193
+ }
101
194
  if (!opts.force) {
102
195
  console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
196
+ await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
103
197
  return;
104
198
  }
105
199
  // The brief is a PHOTOGRAPH: written once, at the moment of the upgrade,
@@ -262,5 +356,6 @@ export async function upgrade(slug, opts) {
262
356
  ]));
263
357
  console.log("");
264
358
  console.log(body(`(rollback: synthesisui add ${slug} --version ${installed})`));
359
+ await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
265
360
  console.log("");
266
361
  }
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.179",
3
+ "version": "0.16.182",
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": {