synthesisui 0.16.170 → 0.16.172

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.
@@ -156,11 +156,21 @@ export async function versionBehind(root, opts = {}) {
156
156
  * injetado no contexto de um agente, e cada linha decorativa é contexto que não é sobre o trabalho
157
157
  * dele.
158
158
  */
159
- export function describeMisalignments(items) {
159
+ export function describeMisalignments(items,
160
+ /**
161
+ * DE ONDE A PERGUNTA VEIO decide a primeira frase, e não é enfeite.
162
+ *
163
+ * "Before this session starts" descreve o hook do editor. Num terminal não existe sessão nenhuma
164
+ * para começar, e a frase vira ruído em cima de um aviso que a pessoa precisa ler - o gancho de
165
+ * shell nasceu em 07/08 e a copy tinha que nascer com ele.
166
+ */
167
+ from = "session") {
160
168
  if (items.length === 0)
161
169
  return "";
162
170
  return [
163
- "Before this session starts, this environment is out of alignment with the design system that governs it:",
171
+ from === "shell"
172
+ ? "This repo is out of alignment with the design system that governs it:"
173
+ : "Before this session starts, this environment is out of alignment with the design system that governs it:",
164
174
  ...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
165
175
  ].join("\n");
166
176
  }
@@ -180,7 +190,7 @@ export async function align(opts) {
180
190
  const remote = await versionBehind(root).catch(() => null);
181
191
  if (remote)
182
192
  items.push(remote);
183
- const text = describeMisalignments(items);
193
+ const text = describeMisalignments(items, opts.shell ? "shell" : "session");
184
194
  if (text)
185
195
  console.log(text);
186
196
  }
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { wireAgent } from "../agent-wiring.js";
4
4
  import { blockHomes, syncClaudeMd } from "../claude-md.js";
5
5
  import { body, paint, section, snippet } from "../output.js";
6
+ import { hasHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
6
7
  import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "../skill-import.js";
7
8
  import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
8
9
  import { add } from "./add.js";
@@ -81,6 +82,59 @@ async function refreshInstall(root, cli, registry) {
81
82
  }
82
83
  return null;
83
84
  }
85
+ /**
86
+ * O AVISO NO TERMINAL, oferecido uma vez - ver `shell-hook.ts` para o porquê e as três restrições.
87
+ *
88
+ * FORA DO REPOSITÓRIO É OUTRO TERRITÓRIO. Tudo que o `connect` escreve hoje mora no projeto, e um
89
+ * `.zshrc` é da PESSOA e de todos os outros projetos dela. Então isto pergunta, mostra exatamente o
90
+ * que vai escrever e onde, e um "não" não custa nada: o hook do editor continua valendo inteiro.
91
+ *
92
+ * Sem TTY (CI, um agente rodando o comando) não há a quem perguntar, e a resposta certa é não
93
+ * escrever - um pipeline não deveria acordar com o perfil de shell de alguém alterado.
94
+ */
95
+ async function offerShellHook(asked) {
96
+ const shell = shellFrom(process.env);
97
+ if (!shell)
98
+ return;
99
+ const rc = rcPathFor(shell);
100
+ const current = await readFile(rc, "utf8").catch(() => null);
101
+ /** Já instalado: re-materializa em silêncio, para uma melhoria no bloco alcançar quem já disse sim. */
102
+ if (current != null && hasHook(current)) {
103
+ const next = withHook(current, shellSnippet(shell));
104
+ if (next !== current) {
105
+ await writeFile(rc, next, "utf8").catch(() => { });
106
+ console.log("");
107
+ console.log(body(`Refreshed the terminal check in ${rc}.`));
108
+ }
109
+ return;
110
+ }
111
+ /** `--shell` é alguém que já decidiu - perguntar de novo seria não ter escutado. */
112
+ if (asked) {
113
+ await writeFile(rc, withHook(current ?? "", shellSnippet(shell)), "utf8");
114
+ console.log("");
115
+ console.log(body(`Terminal check written to ${rc}.`));
116
+ console.log(body(`Open a new terminal, or run: source ${rc}`));
117
+ return;
118
+ }
119
+ console.log("");
120
+ console.log(body("Your editor tells you when this repo drifts, once a session. Your terminal does not, and that is where most people notice something is stale."));
121
+ console.log(body(paint.faint(`Adding it writes one block to ${rc}. It stays silent when nothing is wrong, never blocks your prompt, and checks at most once an hour.`)));
122
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
123
+ console.log(body(paint.blue(" npx synthesisui@latest connect --shell")));
124
+ return;
125
+ }
126
+ const { createInterface } = await import("node:readline/promises");
127
+ const rl = createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout,
130
+ });
131
+ const answer = await rl.question(` Add it to ${rc}? [y/N] `).catch(() => "");
132
+ rl.close();
133
+ if (!/^y(es)?$/i.test(answer.trim()))
134
+ return;
135
+ await writeFile(rc, withHook(current ?? "", shellSnippet(shell)), "utf8");
136
+ console.log(body(`Written. Open a new terminal, or run: source ${rc}`));
137
+ }
84
138
  export async function connect(opts) {
85
139
  const root = opts.dir ?? process.cwd();
86
140
  // Both unless one is explicitly turned off - somebody who says `--no-hook`
@@ -228,6 +282,7 @@ export async function connect(opts) {
228
282
  console.log(body(paint.dim("Your PRs can carry this too: annotations on the exact line, and a ratchet that fails only when drift goes UP - never on the debt you already have.")));
229
283
  console.log(paint.blue(snippet(["npx synthesisui@latest ci"])));
230
284
  }
285
+ await offerShellHook(opts.shell === true);
231
286
  // The step that cost a round trip the first time this was tried by hand,
232
287
  // and would cost every single person one.
233
288
  console.log("");
@@ -1,4 +1,4 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readToken, resolveRegistry } from "../config.js";
4
4
  import { readEvents } from "../doctor/ledger.js";
@@ -248,6 +248,18 @@ export async function remeasure(args) {
248
248
  if (stored.reading)
249
249
  census.reading = stored.reading;
250
250
  await resolveReadParts(census, root).catch(() => { });
251
+ /**
252
+ * O CENSO FRESCO TAMBÉM FICA NO DISCO.
253
+ *
254
+ * O `sync` media, mandava, e não reescrevia `_synthesisui/census.json` - só o `import` escreve. O
255
+ * resultado é a plataforma com a medição de hoje e o disco com a de três dias atrás, e TRÊS
256
+ * comandos locais lendo a velha: `gaps`, `summary`, e a resolução de escopo. Medido em 07/08 no repo
257
+ * do dono: o arquivo era de 06/08 depois de dois syncs (dono, 07/08).
258
+ *
259
+ * Duas cópias da mesma medição que discordam é a família de bug que este repo mais pagou este ano.
260
+ * Falhar aqui não derruba a rodada: o que importava - o envio - já aconteceu.
261
+ */
262
+ await writeFile(join(root, "_synthesisui", "census.json"), `${JSON.stringify(census, null, 2)}\n`, "utf8").catch(() => { });
251
263
  /**
252
264
  * O AVISO, ANTES DE ENVIAR. `edited` são as receitas que uma pessoa escreveu na tela; o cruzamento
253
265
  * com o que acabou de ser medido é uma interseção de conjuntos, e é por isso que ele mora aqui em
package/dist/index.js CHANGED
@@ -246,6 +246,7 @@ async function main() {
246
246
  hook: flags.hook !== false,
247
247
  mcp: flags.mcp !== false,
248
248
  ci: flags.ci === true,
249
+ shell: flags.shell === true,
249
250
  });
250
251
  return;
251
252
  case "hook":
@@ -555,7 +556,7 @@ async function main() {
555
556
  * `align.ts` para os nove estados e por que sete deles não tinham verificação nenhuma.
556
557
  */
557
558
  case "align":
558
- await align({ dir, cli: CLI_VERSION });
559
+ await align({ dir, cli: CLI_VERSION, shell: flags.shell === true });
559
560
  break;
560
561
  case "request": {
561
562
  // `synthesisui request` lists; `request component --name x --for "..."`
@@ -0,0 +1,100 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ /**
4
+ * O AVISO NO TERMINAL, e não só quando o agente abre.
5
+ *
6
+ * O `align` roda como `SessionStart` do editor, o que cobre o momento em que alguém vai trabalhar COM
7
+ * o agente. Não cobre o momento em que a maioria das pessoas percebe que algo está velho, que é abrir
8
+ * o terminal e dar `cd` no projeto (dono, 07/08). Entre um e outro cabe um dia inteiro de trabalho
9
+ * sobre um design system desatualizado - foi exatamente o que aconteceu com a v2 dele.
10
+ *
11
+ * TRÊS RESTRIÇÕES, e as três vieram de medir em vez de supor:
12
+ *
13
+ * 4 segundos é o que um `align` custa através do npx. Rodar em primeiro plano transformaria
14
+ * todo `cd` numa espera, então o gancho joga para segundo plano e nunca segura o
15
+ * prompt.
16
+ * uma vez por sem trava, entrar e sair de uma pasta dispararia uma chamada de rede por vez. O
17
+ * hora carimbo mora em `~/.cache/synthesisui`, FORA do repositório - um arquivo de
18
+ * controle nosso dentro do projeto de alguém vira linha no `git status` dela.
19
+ * silêncio o `align` já não imprime nada quando está tudo certo, e é isso que faz um aviso
20
+ * continuar sendo lido no dia em que importa.
21
+ *
22
+ * O bloco é DELIMITADO por marcas. Sem elas, atualizar significaria pedir para a pessoa editar o
23
+ * próprio `.zshrc` à mão, e instalar duas vezes deixaria duas cópias.
24
+ */
25
+ export const HOOK_BEGIN = "# >>> synthesisui align >>>";
26
+ export const HOOK_END = "# <<< synthesisui align <<<";
27
+ /**
28
+ * O bloco a escrever. `shell` decide só a forma de registrar o gancho de diretório: o zsh tem
29
+ * `add-zsh-hook chpwd`, o bash tem `PROMPT_COMMAND` e nenhum equivalente de `chpwd`, então lá a
30
+ * função se protege sozinha comparando o diretório com o da última vez.
31
+ */
32
+ export function shellSnippet(shell) {
33
+ const body = shell === "zsh"
34
+ ? [
35
+ "sui_align() {",
36
+ " [[ -d _synthesisui ]] || return",
37
+ ' local dir="$HOME/.cache/synthesisui" key="${PWD//\\//_}"',
38
+ ' mkdir -p "$dir"',
39
+ ' local stamp="$dir/align$key"',
40
+ ' if [[ -f "$stamp" ]]; then',
41
+ ' (( $(date +%s) - $(stat -c %Y "$stamp" 2>/dev/null || echo 0) < 3600 )) && return',
42
+ " fi",
43
+ ' : > "$stamp"',
44
+ " (npx --yes synthesisui align --shell 2>/dev/null &)",
45
+ "}",
46
+ "autoload -U add-zsh-hook",
47
+ "add-zsh-hook chpwd sui_align",
48
+ "sui_align",
49
+ ]
50
+ : [
51
+ "sui_align() {",
52
+ " [ -d _synthesisui ] || return 0",
53
+ ' [ "$PWD" = "$SUI_LAST_DIR" ] && return 0',
54
+ ' SUI_LAST_DIR="$PWD"',
55
+ ' local dir="$HOME/.cache/synthesisui" key=$(echo "$PWD" | tr / _)',
56
+ ' mkdir -p "$dir"',
57
+ ' local stamp="$dir/align$key"',
58
+ ' if [ -f "$stamp" ]; then',
59
+ ' [ $(( $(date +%s) - $(stat -c %Y "$stamp" 2>/dev/null || echo 0) )) -lt 3600 ] && return 0',
60
+ " fi",
61
+ ' : > "$stamp"',
62
+ " (npx --yes synthesisui align --shell 2>/dev/null &)",
63
+ "}",
64
+ 'case "$PROMPT_COMMAND" in *sui_align*) ;; *) PROMPT_COMMAND="sui_align;$PROMPT_COMMAND" ;; esac',
65
+ ];
66
+ return [
67
+ HOOK_BEGIN,
68
+ "# Tells you when this repo drifts from the design system that governs it.",
69
+ "# Silent when nothing is wrong. Never blocks the prompt; checks once an hour.",
70
+ "# Remove this block to uninstall - nothing else on disk depends on it.",
71
+ ...body,
72
+ HOOK_END,
73
+ ].join("\n");
74
+ }
75
+ /** Qual arquivo de perfil, a partir do shell que a pessoa está rodando. */
76
+ export function rcPathFor(shell, home = homedir()) {
77
+ return join(home, shell === "zsh" ? ".zshrc" : ".bashrc");
78
+ }
79
+ /** Qual shell, lido do ambiente. `null` quando não é um dos dois - aí não há o que escrever. */
80
+ export function shellFrom(env) {
81
+ const name = (env.SHELL ?? "").split("/").pop();
82
+ return name === "zsh" || name === "bash" ? name : null;
83
+ }
84
+ export function hasHook(text) {
85
+ return text.includes(HOOK_BEGIN);
86
+ }
87
+ /**
88
+ * O arquivo com o bloco de HOJE. Instalar de novo SUBSTITUI o bloco antigo em vez de acrescentar um
89
+ * segundo - senão uma melhoria no gancho viraria duas funções brigando pelo mesmo nome.
90
+ */
91
+ export function withHook(text, snippet) {
92
+ if (!hasHook(text))
93
+ return `${text.replace(/\n*$/, "")}\n\n${snippet}\n`.replace(/^\n+/, "");
94
+ const start = text.indexOf(HOOK_BEGIN);
95
+ const endMark = text.indexOf(HOOK_END, start);
96
+ /** Marca de abertura sem a de fechamento: alguém editou à mão, e apagar até o fim seria destrutivo. */
97
+ if (endMark === -1)
98
+ return text;
99
+ return text.slice(0, start) + snippet + text.slice(endMark + HOOK_END.length);
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.170",
3
+ "version": "0.16.172",
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": {