synthesisui 0.16.171 → 0.16.174
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.
- package/dist/commands/add.js +2 -0
- package/dist/commands/align.js +41 -9
- package/dist/commands/connect.js +80 -1
- package/dist/doctor/requests.js +81 -12
- package/dist/index.js +2 -1
- package/dist/shell-hook.js +100 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -137,6 +137,8 @@ export async function add(slug, opts) {
|
|
|
137
137
|
registry: base,
|
|
138
138
|
fetchedAt: new Date().toISOString(),
|
|
139
139
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
140
|
+
/** Ver `RegistryPayload.compiler`: é o que faz um conserto de CSS chegar a um install. */
|
|
141
|
+
...(payload.compiler != null ? { compiler: payload.compiler } : {}),
|
|
140
142
|
...(scope ? { scope } : {}),
|
|
141
143
|
...(usage.length > 0 ? { usage } : {}),
|
|
142
144
|
};
|
package/dist/commands/align.js
CHANGED
|
@@ -138,16 +138,38 @@ export async function versionBehind(root, opts = {}) {
|
|
|
138
138
|
const token = await readToken();
|
|
139
139
|
if (!token)
|
|
140
140
|
return null;
|
|
141
|
-
|
|
141
|
+
/**
|
|
142
|
+
* `?meta=1` - a versão e o compilador, e nada mais.
|
|
143
|
+
*
|
|
144
|
+
* Isto baixava o payload INTEIRO para ler um número: o documento, três CSS compilados e o GUIDE,
|
|
145
|
+
* a cada abertura de sessão e a cada entrada na pasta pelo terminal.
|
|
146
|
+
*/
|
|
147
|
+
const res = await fetch(`${lock.registry ?? resolveRegistry()}/api/registry/ds/${lock.slug}?meta=1`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
|
|
142
148
|
if (!res?.ok)
|
|
143
149
|
return null;
|
|
144
150
|
const body = (await res.json().catch(() => null));
|
|
145
|
-
if (!body?.version
|
|
151
|
+
if (!body?.version)
|
|
146
152
|
return null;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
if (body.version > lock.version)
|
|
154
|
+
return {
|
|
155
|
+
says: `v${body.version} of "${lock.slug}" is published and this repo is on v${lock.version}. The CSS here and the rules your agent reads are both v${lock.version} - they move together, which is why this is worth saying rather than applying.`,
|
|
156
|
+
run: `npx synthesisui upgrade ${lock.slug}`,
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* A MESMA VERSÃO, COMPILADA DIFERENTE - o caso que não tinha como ser dito nem consertado.
|
|
160
|
+
*
|
|
161
|
+
* O CSS não é congelado na publicação: ele é compilado do documento a cada busca. Então um conserto
|
|
162
|
+
* nosso vale para uma versão JÁ publicada, e nenhum comando o buscava - `upgrade` age por versão e
|
|
163
|
+
* `connect` agia por CLI. Em 07/08 o bloco de esquema alternativo passou a emitir os dois
|
|
164
|
+
* ancestrais e não havia caminho até o disco de quem já tinha instalado.
|
|
165
|
+
*/
|
|
166
|
+
if (typeof body.compiler === "number" &&
|
|
167
|
+
body.compiler !== (lock.compiler ?? null))
|
|
168
|
+
return {
|
|
169
|
+
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.`,
|
|
170
|
+
run: "npx synthesisui connect",
|
|
171
|
+
};
|
|
172
|
+
return null;
|
|
151
173
|
}
|
|
152
174
|
/**
|
|
153
175
|
* A linha que a sessão vê.
|
|
@@ -156,11 +178,21 @@ export async function versionBehind(root, opts = {}) {
|
|
|
156
178
|
* injetado no contexto de um agente, e cada linha decorativa é contexto que não é sobre o trabalho
|
|
157
179
|
* dele.
|
|
158
180
|
*/
|
|
159
|
-
export function describeMisalignments(items
|
|
181
|
+
export function describeMisalignments(items,
|
|
182
|
+
/**
|
|
183
|
+
* DE ONDE A PERGUNTA VEIO decide a primeira frase, e não é enfeite.
|
|
184
|
+
*
|
|
185
|
+
* "Before this session starts" descreve o hook do editor. Num terminal não existe sessão nenhuma
|
|
186
|
+
* para começar, e a frase vira ruído em cima de um aviso que a pessoa precisa ler - o gancho de
|
|
187
|
+
* shell nasceu em 07/08 e a copy tinha que nascer com ele.
|
|
188
|
+
*/
|
|
189
|
+
from = "session") {
|
|
160
190
|
if (items.length === 0)
|
|
161
191
|
return "";
|
|
162
192
|
return [
|
|
163
|
-
|
|
193
|
+
from === "shell"
|
|
194
|
+
? "This repo is out of alignment with the design system that governs it:"
|
|
195
|
+
: "Before this session starts, this environment is out of alignment with the design system that governs it:",
|
|
164
196
|
...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
|
|
165
197
|
].join("\n");
|
|
166
198
|
}
|
|
@@ -180,7 +212,7 @@ export async function align(opts) {
|
|
|
180
212
|
const remote = await versionBehind(root).catch(() => null);
|
|
181
213
|
if (remote)
|
|
182
214
|
items.push(remote);
|
|
183
|
-
const text = describeMisalignments(items);
|
|
215
|
+
const text = describeMisalignments(items, opts.shell ? "shell" : "session");
|
|
184
216
|
if (text)
|
|
185
217
|
console.log(text);
|
|
186
218
|
}
|
package/dist/commands/connect.js
CHANGED
|
@@ -2,7 +2,9 @@ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { wireAgent } from "../agent-wiring.js";
|
|
4
4
|
import { blockHomes, syncClaudeMd } from "../claude-md.js";
|
|
5
|
+
import { resolveRegistry } from "../config.js";
|
|
5
6
|
import { body, paint, section, snippet } from "../output.js";
|
|
7
|
+
import { hasHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
|
|
6
8
|
import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "../skill-import.js";
|
|
7
9
|
import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
|
|
8
10
|
import { add } from "./add.js";
|
|
@@ -47,6 +49,14 @@ const LEGACY_SKILLS = ["import-design-system"];
|
|
|
47
49
|
* SILENCIOSO QUANDO JÁ ESTÁ EM DIA, e sem rede não faz nada - um `connect` que falha por estar num
|
|
48
50
|
* avião seria pior que a defasagem que ele conserta.
|
|
49
51
|
*/
|
|
52
|
+
/** O número do compilador que serviria esta versão HOJE - `null` quando não dá para saber. */
|
|
53
|
+
async function fetchCompiler(base, slug, version) {
|
|
54
|
+
const res = await fetch(`${base}/api/registry/ds/${slug}?version=${version}&meta=1`).catch(() => null);
|
|
55
|
+
if (!res?.ok)
|
|
56
|
+
return null;
|
|
57
|
+
const body = (await res.json().catch(() => null));
|
|
58
|
+
return typeof body?.compiler === "number" ? body.compiler : null;
|
|
59
|
+
}
|
|
50
60
|
async function refreshInstall(root, cli, registry) {
|
|
51
61
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
52
62
|
const names = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
|
|
@@ -66,7 +76,22 @@ async function refreshInstall(root, cli, registry) {
|
|
|
66
76
|
/** Um DS adotado é descrito aqui e pertencido em outro lugar - não há o que rematerializar. */
|
|
67
77
|
if (!lock.slug || lock.adopted || typeof lock.version !== "number")
|
|
68
78
|
continue;
|
|
69
|
-
|
|
79
|
+
/**
|
|
80
|
+
* O COMPILADOR TAMBÉM DECIDE, e sem ele um conserto de CSS não alcançava ninguém.
|
|
81
|
+
*
|
|
82
|
+
* `tokens.css` é compilado no servidor a cada busca, então ele melhora sem que uma linha do CLI
|
|
83
|
+
* mude e sem que a versão do sistema ande. Aí `upgrade` (que age por versão) e este bloco (que
|
|
84
|
+
* agia só por CLI) passavam batido, e o conserto ficava disponível para sempre sem caminho até o
|
|
85
|
+
* disco de quem instalou - foi o que aconteceu em 07/08 com o seletor do esquema alternativo.
|
|
86
|
+
*
|
|
87
|
+
* `?meta=1` custa algumas centenas de bytes; falhar nele não pode custar o `connect`, então um
|
|
88
|
+
* erro de rede simplesmente não aciona a rematerialização.
|
|
89
|
+
*/
|
|
90
|
+
/** O host que emitiu ESTE install manda - um token pertence ao host que o emitiu. */
|
|
91
|
+
const base = resolveRegistry(registry ?? lock.registry);
|
|
92
|
+
const remote = await fetchCompiler(base, lock.slug, lock.version);
|
|
93
|
+
const compilerMoved = remote != null && remote !== (lock.compiler ?? null);
|
|
94
|
+
if (lock.cli === cli && !compilerMoved)
|
|
70
95
|
continue;
|
|
71
96
|
const done = await add(lock.slug, {
|
|
72
97
|
...(registry ? { registry } : {}),
|
|
@@ -81,6 +106,59 @@ async function refreshInstall(root, cli, registry) {
|
|
|
81
106
|
}
|
|
82
107
|
return null;
|
|
83
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* O AVISO NO TERMINAL, oferecido uma vez - ver `shell-hook.ts` para o porquê e as três restrições.
|
|
111
|
+
*
|
|
112
|
+
* FORA DO REPOSITÓRIO É OUTRO TERRITÓRIO. Tudo que o `connect` escreve hoje mora no projeto, e um
|
|
113
|
+
* `.zshrc` é da PESSOA e de todos os outros projetos dela. Então isto pergunta, mostra exatamente o
|
|
114
|
+
* que vai escrever e onde, e um "não" não custa nada: o hook do editor continua valendo inteiro.
|
|
115
|
+
*
|
|
116
|
+
* Sem TTY (CI, um agente rodando o comando) não há a quem perguntar, e a resposta certa é não
|
|
117
|
+
* escrever - um pipeline não deveria acordar com o perfil de shell de alguém alterado.
|
|
118
|
+
*/
|
|
119
|
+
async function offerShellHook(asked) {
|
|
120
|
+
const shell = shellFrom(process.env);
|
|
121
|
+
if (!shell)
|
|
122
|
+
return;
|
|
123
|
+
const rc = rcPathFor(shell);
|
|
124
|
+
const current = await readFile(rc, "utf8").catch(() => null);
|
|
125
|
+
/** Já instalado: re-materializa em silêncio, para uma melhoria no bloco alcançar quem já disse sim. */
|
|
126
|
+
if (current != null && hasHook(current)) {
|
|
127
|
+
const next = withHook(current, shellSnippet(shell));
|
|
128
|
+
if (next !== current) {
|
|
129
|
+
await writeFile(rc, next, "utf8").catch(() => { });
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(body(`Refreshed the terminal check in ${rc}.`));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
/** `--shell` é alguém que já decidiu - perguntar de novo seria não ter escutado. */
|
|
136
|
+
if (asked) {
|
|
137
|
+
await writeFile(rc, withHook(current ?? "", shellSnippet(shell)), "utf8");
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(body(`Terminal check written to ${rc}.`));
|
|
140
|
+
console.log(body(`Open a new terminal, or run: source ${rc}`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log("");
|
|
144
|
+
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."));
|
|
145
|
+
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.`)));
|
|
146
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
147
|
+
console.log(body(paint.blue(" npx synthesisui@latest connect --shell")));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const { createInterface } = await import("node:readline/promises");
|
|
151
|
+
const rl = createInterface({
|
|
152
|
+
input: process.stdin,
|
|
153
|
+
output: process.stdout,
|
|
154
|
+
});
|
|
155
|
+
const answer = await rl.question(` Add it to ${rc}? [y/N] `).catch(() => "");
|
|
156
|
+
rl.close();
|
|
157
|
+
if (!/^y(es)?$/i.test(answer.trim()))
|
|
158
|
+
return;
|
|
159
|
+
await writeFile(rc, withHook(current ?? "", shellSnippet(shell)), "utf8");
|
|
160
|
+
console.log(body(`Written. Open a new terminal, or run: source ${rc}`));
|
|
161
|
+
}
|
|
84
162
|
export async function connect(opts) {
|
|
85
163
|
const root = opts.dir ?? process.cwd();
|
|
86
164
|
// Both unless one is explicitly turned off - somebody who says `--no-hook`
|
|
@@ -228,6 +306,7 @@ export async function connect(opts) {
|
|
|
228
306
|
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
307
|
console.log(paint.blue(snippet(["npx synthesisui@latest ci"])));
|
|
230
308
|
}
|
|
309
|
+
await offerShellHook(opts.shell === true);
|
|
231
310
|
// The step that cost a round trip the first time this was tried by hand,
|
|
232
311
|
// and would cost every single person one.
|
|
233
312
|
console.log("");
|
package/dist/doctor/requests.js
CHANGED
|
@@ -32,7 +32,20 @@ function idOf(kind, name, at) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function fileRequest(root, req) {
|
|
34
34
|
const at = req.at ?? new Date().toISOString();
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* O CARIMBO SÓ PODE SER FEITO AGORA: depois que alguém edita o sistema, não há como saber se o nome
|
|
37
|
+
* existia quando o pedido nasceu - e é essa a diferença entre "apareceu" e "sempre esteve lá".
|
|
38
|
+
*/
|
|
39
|
+
const name = checkableName({ ...req, at, id: "" });
|
|
40
|
+
const existed = name
|
|
41
|
+
? declaresName(await readInstalledCss(root), name)
|
|
42
|
+
: false;
|
|
43
|
+
const full = {
|
|
44
|
+
...req,
|
|
45
|
+
at,
|
|
46
|
+
id: idOf(req.kind, req.name, at),
|
|
47
|
+
...(existed ? { existed: true } : {}),
|
|
48
|
+
};
|
|
36
49
|
try {
|
|
37
50
|
await appendFile(path(root), `${JSON.stringify(full)}\n`, "utf8");
|
|
38
51
|
}
|
|
@@ -94,24 +107,80 @@ export function checkableName(req) {
|
|
|
94
107
|
const m = /[a-z][a-z0-9]*(?:-[a-z0-9]+)+/.exec(req.name.trim());
|
|
95
108
|
return m ? m[0] : null;
|
|
96
109
|
}
|
|
97
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Cada `--custom-property: valor` que o css instalado declara. A ÚLTIMA vence, que é o que a cascata
|
|
112
|
+
* faz para declarações do mesmo peso - ler a primeira responderia sobre um bloco que o navegador
|
|
113
|
+
* descarta.
|
|
114
|
+
*/
|
|
115
|
+
function declaredValues(css) {
|
|
116
|
+
const out = new Map();
|
|
117
|
+
const re = /(--[a-z0-9-]+)\s*:\s*([^;}]+)/gi;
|
|
118
|
+
let m = re.exec(css);
|
|
119
|
+
while (m) {
|
|
120
|
+
out.set(m[1].toLowerCase(), m[2].trim());
|
|
121
|
+
m = re.exec(css);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* O valor final de uma declaração, seguindo `var()` enquanto der. Um design system nomeia um valor
|
|
127
|
+
* apontando um papel para uma primitiva, então o valor pedido quase nunca está escrito na linha do
|
|
128
|
+
* papel - parar no primeiro `var()` reprovaria justamente o caso comum.
|
|
129
|
+
*/
|
|
130
|
+
function resolved(map, value, hops = 5) {
|
|
131
|
+
let v = value.trim();
|
|
132
|
+
for (let i = 0; i < hops; i += 1) {
|
|
133
|
+
const m = /^var\(\s*(--[a-z0-9-]+)/i.exec(v);
|
|
134
|
+
if (!m)
|
|
135
|
+
break;
|
|
136
|
+
const next = map.get(m[1].toLowerCase());
|
|
137
|
+
if (next == null)
|
|
138
|
+
break;
|
|
139
|
+
v = next.trim();
|
|
140
|
+
}
|
|
141
|
+
return v.toLowerCase().replace(/\s+/g, " ");
|
|
142
|
+
}
|
|
143
|
+
const candidatesFor = (name) => name.startsWith("animate-") ? [`--${name}`] : [`--ds-${name}`, `--${name}`];
|
|
144
|
+
/** Os nomes que o css instalado JÁ declara - ver `GapRequest.existed`. */
|
|
145
|
+
export function declaresName(installedCss, name) {
|
|
146
|
+
const declared = declaredValues(installedCss);
|
|
147
|
+
return candidatesFor(name).some((c) => declared.has(c.toLowerCase()));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Requests whose ask the installed css now demonstrably delivers.
|
|
151
|
+
*
|
|
152
|
+
* APARECER É PROVA; EXISTIR NÃO É - e a diferença fechou um bug real como entregue.
|
|
153
|
+
*
|
|
154
|
+
* Um pedido é "isto não tem nome". Quando o nome de fato não existia, ele passar a existir é prova
|
|
155
|
+
* suficiente e é o caso comum - `animate-rise` não tem um valor comparável, o pedido é pelo NOME.
|
|
156
|
+
*
|
|
157
|
+
* Mas um pedido feito SOBRE um token que já existe é sobre o VALOR dele, e aí existir responde sim
|
|
158
|
+
* no instante em que o pedido nasce. Em 07/08 um agente reportou `color-semantic-canvas` apontando
|
|
159
|
+
* para a mesma primitiva que o `foreground` - 1.00:1, texto invisível - e o `sync` seguinte imprimiu
|
|
160
|
+
* *"delivered. Nothing for anyone to do"* com o defeito intacto no disco do dono.
|
|
161
|
+
*
|
|
162
|
+
* Então `existed` é carimbado quando o pedido é feito, e um pedido assim só fecha quando o css
|
|
163
|
+
* ENTREGA o valor pedido. Sem valor comparável ele fica aberto e quem decide é a pessoa, no cartão -
|
|
164
|
+
* um pedido aberto custa uma linha numa tela, um fechamento falso custa o bug.
|
|
165
|
+
*/
|
|
98
166
|
export function satisfiedRequests(requests, installedCss) {
|
|
167
|
+
const declared = declaredValues(installedCss);
|
|
99
168
|
return requests.filter((r) => {
|
|
100
169
|
const name = checkableName(r);
|
|
101
170
|
if (!name)
|
|
102
171
|
return false;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
172
|
+
const present = candidatesFor(name).filter((c) => declared.has(c.toLowerCase()));
|
|
173
|
+
if (present.length === 0)
|
|
174
|
+
return false;
|
|
175
|
+
if (!r.existed)
|
|
176
|
+
return true;
|
|
177
|
+
const want = r.value?.trim();
|
|
178
|
+
if (!want)
|
|
179
|
+
return false;
|
|
180
|
+
const asked = resolved(declared, want);
|
|
181
|
+
return present.some((c) => resolved(declared, declared.get(c.toLowerCase()) ?? "") === asked);
|
|
107
182
|
});
|
|
108
183
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Everything the pinned install actually ships, as one string - tokens.css
|
|
111
|
-
* AND theme.css of the locked version, for every installed system. This is
|
|
112
|
-
* the ground verification stands on: not the doc, not the GUIDE's promises,
|
|
113
|
-
* the css a build would really read.
|
|
114
|
-
*/
|
|
115
184
|
export async function readInstalledCss(root) {
|
|
116
185
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
117
186
|
let css = "";
|
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