synthesisui 0.16.445 → 0.16.447

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,8 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import { dirname, join } from "node:path";
3
4
  import { hasCodexBlock, pinnedInCodex, withCodexBlock } from "./codex-mcp.js";
5
+ import { wrapperSource } from "./statusline-wrapper.js";
4
6
  /**
5
7
  * QUANDO A CHECAGEM RODA - e por que `Bash` entrou em 12/09.
6
8
  *
@@ -326,6 +328,64 @@ async function wireCodexMcp(root) {
326
328
  await writeFile(path, next, "utf8");
327
329
  return had ? "updated" : "added";
328
330
  }
331
+ /**
332
+ * A BARRA DE STATUS GANHA UM SEGMENTO, E A DELE CONTINUA INTEIRA - pedido do dono em 21/09:
333
+ * *"uma métrica um pouco na frente do Status que fale a quantidade de vezes que a gente
334
+ * sincronizou (...) se tiver alguma coisa lá, não mexe: mantém o que estava e adiciona"*.
335
+ *
336
+ * O `statusLine` do `.claude/settings.json` é DELE - no caso do dono, o Pulse, que vem do CodeLevel.
337
+ * Por isso não se substitui nada: o que se escreve é um invólucro que CHAMA o comando que estava lá
338
+ * e acrescenta o nosso segmento no fim. Se aquele comando cair, a barra dele ainda sai; se o ledger
339
+ * não puder ser lido, o segmento some e o resto fica.
340
+ *
341
+ * IDEMPOTENTE PELA MARCA: se o `statusLine` já aponta para o nosso arquivo, nada é reescrito - nem
342
+ * o `settings.json`, nem o invólucro, que guardaria o comando ERRADO se fosse regravado (ele já
343
+ * aponta para si mesmo, e o anterior se perderia).
344
+ */
345
+ export async function wireStatusLine(root,
346
+ /**
347
+ * A CASA, INJETADA - e não lida de `process.env` dentro do teste.
348
+ *
349
+ * O teste precisa de um `~/.claude/settings.json` de mentira, e a primeira versão trocava o
350
+ * `HOME` do processo para consegui-lo. Duas coisas erradas nisso: o gate de provas dependentes de
351
+ * máquina passa a ver este spec como quem lê a casa de quem roda - e ele estaria certo, porque
352
+ * trocar env é global e vaza para o que roda em paralelo ao lado.
353
+ */
354
+ home = homedir()) {
355
+ const rel = "_synthesisui/statusline.mjs";
356
+ const ours = `node ${rel}`;
357
+ const dir = join(root, ".claude");
358
+ const path = join(dir, "settings.json");
359
+ const settings = await readJson(path);
360
+ const current = settings.statusLine;
361
+ const command = typeof current?.command === "string" ? current.command : "";
362
+ if (command.includes(rel))
363
+ return { status: "already there", command: ours };
364
+ /**
365
+ * A BARRA DELE PODE MORAR NO SETTINGS GLOBAL, E QUASE SEMPRE MORA - achado em 21/09, com o print
366
+ * dele na mão e o invólucro já instalado.
367
+ *
368
+ * A primeira versão olhava só o `.claude/settings.json` DO PROJETO. No repositório dele não havia
369
+ * `statusLine` ali - o Pulse está em `~/.claude/settings.json` -, então o invólucro nasceu com
370
+ * "não havia barra anterior" e deixaria de chamar o Pulse. Na sessão seguinte ele perderia a barra
371
+ * inteira e veria só o nosso segmento: exatamente o contrário do que ele pediu.
372
+ *
373
+ * O Claude Code resolve `statusLine` do projeto SOBRE o global, então o que a gente substitui é o
374
+ * que estava valendo ali - e o que estava valendo é o global quando o projeto cala.
375
+ */
376
+ const globalPath = join(home, ".claude", "settings.json");
377
+ const globalSettings = await readJson(globalPath);
378
+ const globalLine = globalSettings.statusLine;
379
+ const fallback = typeof globalLine?.command === "string" ? globalLine.command : "";
380
+ const was = command.trim() ? command : fallback.trim() ? fallback : null;
381
+ await mkdir(join(root, "_synthesisui"), { recursive: true });
382
+ await writeFile(join(root, rel), wrapperSource(was), "utf8");
383
+ await mkdir(dir, { recursive: true });
384
+ await writeFile(path, `${JSON.stringify({ ...settings, statusLine: { type: "command", command: ours } }, null, 2)}\n`, "utf8");
385
+ return was
386
+ ? { status: "wrapped", command: ours, was }
387
+ : { status: "added", command: ours };
388
+ }
329
389
  export async function wireAgent(root, version, want) {
330
390
  /**
331
391
  * A ESCOLHA DELE DECIDE QUEM É FIADO (A2 da etapa 12) - ver `agents-chosen.ts`.
@@ -355,6 +415,14 @@ export async function wireAgent(root, version, want) {
355
415
  ? await wireSessionStart(root, await alignCommand(root, version))
356
416
  : "skipped",
357
417
  mcp: want.mcp ? await wireMcp(root, want.agents) : "skipped",
418
+ /**
419
+ * O SEGMENTO DA BARRA vai junto do resto da fiação do Claude Code, e pelo mesmo motivo: o
420
+ * `settings.json` é dele, e num repositório onde ele não escolheu o Claude Code escrever ali é
421
+ * plantar fiação que ninguém pediu.
422
+ */
423
+ statusLine: claude
424
+ ? await wireStatusLine(root)
425
+ : { status: "skipped", command: "" },
358
426
  };
359
427
  }
360
428
  /**
@@ -0,0 +1,105 @@
1
+ /**
2
+ * A BARRA DELE CONTINUA SENDO A DELE, E A NOSSA INFORMAÇÃO ENTRA AO LADO - pedido do dono em
3
+ * 21/09: *"se tiver alguma coisa lá, ele não mexesse; mantivesse o que estava lá e adicionasse
4
+ * mais essa informação"*.
5
+ *
6
+ * O `statusLine` do `.claude/settings.json` costuma já ter um script - no caso dele, o Pulse, que é
7
+ * do CodeLevel. Trocar aquilo pelo nosso seria apagar a barra que ele construiu. Então o que a
8
+ * gente instala é um INVÓLUCRO: ele chama o comando que estava lá, pega a saída inteira e acrescenta
9
+ * o nosso segmento no fim da última linha.
10
+ *
11
+ * ═══ O QUE ESTE MÓDULO É ═══
12
+ *
13
+ * A função que gera o TEXTO do invólucro. Ela existe para o conteúdo do arquivo ser testável sem
14
+ * escrever em disco nem rodar um shell - o que o teste guarda é que o comando anterior é chamado e
15
+ * que a falha dele não derruba a barra.
16
+ */
17
+ /** A marca que torna a instalação idempotente e a remoção segura. */
18
+ export const WRAPPER_MARK = "synthesisui:statusline";
19
+ /**
20
+ * O invólucro, como texto.
21
+ *
22
+ * `anterior` é o comando que já estava no `statusLine` - `null` quando não havia nenhum, e aí a
23
+ * barra passa a ser só o nosso segmento.
24
+ */
25
+ export function wrapperSource(anterior) {
26
+ return `#!/usr/bin/env node
27
+ /* ${WRAPPER_MARK} - managed by the synthesisui CLI. Delete this file to remove it.
28
+ *
29
+ * It runs YOUR status line and appends one segment: how many pulses today and in total.
30
+ * Your command is called first and its output is passed through untouched - if it fails,
31
+ * or if the count cannot be read, you still get your bar. A status line that disappears
32
+ * because a counter broke is worse than no counter.
33
+ */
34
+ import { execSync } from "node:child_process";
35
+ import { readFileSync } from "node:fs";
36
+ import { join } from "node:path";
37
+
38
+ const input = readFileSync(0, "utf8");
39
+ const previous = ${anterior === null ? "null" : JSON.stringify(anterior)};
40
+
41
+ let bar = "";
42
+ if (previous) {
43
+ try {
44
+ /* DOIS SEGUNDOS, E O NÚMERO É AJUSTÁVEL: a barra redesenha o tempo todo, então esperar por ela
45
+ é pagar em travamento do terminal. Quem tem uma barra pesada afrouxa por env em vez de
46
+ perder o segmento - e o teste usa isso para não competir com a suíte inteira. */
47
+ const limit = Number(process.env.SYNTHESISUI_BAR_TIMEOUT_MS) || 2000;
48
+ bar = execSync(previous, { input, encoding: "utf8", timeout: limit });
49
+ } catch {
50
+ /* a barra dele caiu ou demorou: seguimos com o nosso segmento, nunca com a tela vazia */
51
+ }
52
+ }
53
+
54
+ let segment = "";
55
+ try {
56
+ const dir = process.env.SYNTHESISUI_DIR ?? "_synthesisui";
57
+ const raw = readFileSync(join(process.cwd(), dir, "ledger.jsonl"), "utf8");
58
+ const lines = raw
59
+ .split("\\n")
60
+ .filter(Boolean)
61
+ .map((l) => {
62
+ try {
63
+ return JSON.parse(l);
64
+ } catch {
65
+ return null;
66
+ }
67
+ });
68
+ /* A CONTAGEM VIAJA INLINE, e não como import: um segundo arquivo ao lado é mais uma peça para
69
+ sumir, e este script roda a cada respiração do terminal. A regra está em pulse-count.ts, com
70
+ os testes; aqui ela é a mesma condição, escrita uma vez. */
71
+ const FETCHING = new Set(["import", "add", "component", "template", "upgrade", "sync", "absorb", "refit"]);
72
+ const now = new Date();
73
+ let today = 0;
74
+ let total = 0;
75
+ for (const l of lines) {
76
+ if (!l || l.kind !== "run" || typeof l.command !== "string") continue;
77
+ if (l.ok === false || !FETCHING.has(l.command) || typeof l.at !== "string") continue;
78
+ const when = new Date(l.at);
79
+ if (Number.isNaN(when.getTime())) continue;
80
+ total += 1;
81
+ if (
82
+ when.getFullYear() === now.getFullYear() &&
83
+ when.getMonth() === now.getMonth() &&
84
+ when.getDate() === now.getDate()
85
+ )
86
+ today += 1;
87
+ }
88
+ /* Zero e zero não vira "0 · 0": não há o que dizer ainda, e um zero parece defeito. */
89
+ if (total > 0) segment = \`Pulls \${today} today · \${total}\`;
90
+ } catch {
91
+ /* sem ledger ainda, ou ilegível: o segmento simplesmente não aparece */
92
+ }
93
+
94
+ if (!segment) {
95
+ process.stdout.write(bar);
96
+ } else if (!bar.trim()) {
97
+ process.stdout.write(segment);
98
+ } else {
99
+ /* No fim da ÚLTIMA linha: é lá que a barra dele termina, e é onde ele pediu - à frente do Status. */
100
+ const lines = bar.replace(/\\n$/, "").split("\\n");
101
+ lines[lines.length - 1] += " │ " + segment;
102
+ process.stdout.write(lines.join("\\n"));
103
+ }
104
+ `;
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.445",
3
+ "version": "0.16.447",
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": {