synthesisui 0.16.158 → 0.16.160
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/agent-wiring.js +52 -0
- package/dist/commands/add.js +44 -0
- package/dist/commands/align.js +125 -0
- package/dist/commands/connect.js +9 -0
- package/dist/commands/import.js +16 -0
- package/dist/commands/mcp.js +18 -1
- package/dist/commands/sync.js +23 -4
- package/dist/index.js +8 -0
- package/dist/measured-scope.js +59 -1
- package/package.json +1 -1
package/dist/agent-wiring.js
CHANGED
|
@@ -36,6 +36,22 @@ export async function hookCommand(root, version) {
|
|
|
36
36
|
? "npx --no-install synthesisui hook"
|
|
37
37
|
: `npx synthesisui@${version} hook`;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* COMO A SESSÃO CHAMA O CLI, e por que este NÃO é pinado.
|
|
41
|
+
*
|
|
42
|
+
* O hook de escrita é pinado porque roda dezenas de vezes por sessão e um verificador que muda sob
|
|
43
|
+
* você a cada edit é indebugável. Este roda UMA vez, e o trabalho dele é justamente dizer o que está
|
|
44
|
+
* atrasado - inclusive o próprio CLI. Um `align` pinado no dia do `connect` não conhece nenhuma
|
|
45
|
+
* verificação escrita depois, e passa a garantir silêncio em vez de alinho.
|
|
46
|
+
*
|
|
47
|
+
* O custo do `npx` (720ms-1.5s, medido em 27/07) é pago uma vez na abertura, não por escrita.
|
|
48
|
+
*/
|
|
49
|
+
export async function alignCommand(root) {
|
|
50
|
+
const local = join(root, "node_modules", ".bin", "synthesisui");
|
|
51
|
+
return (await exists(local))
|
|
52
|
+
? "npx --no-install synthesisui align"
|
|
53
|
+
: "npx synthesisui@latest align";
|
|
54
|
+
}
|
|
39
55
|
/**
|
|
40
56
|
* Returns the command that IS in the file, not the one we would have written.
|
|
41
57
|
*
|
|
@@ -211,9 +227,45 @@ export async function wireAgent(root, version, want) {
|
|
|
211
227
|
command: hook.command,
|
|
212
228
|
hook: hook.status,
|
|
213
229
|
...(hook.was ? { was: hook.was } : {}),
|
|
230
|
+
/**
|
|
231
|
+
* A VERIFICAÇÃO DE ABERTURA vai junto do hook de escrita, e por um motivo medido: as onze
|
|
232
|
+
* ferramentas MCP são todas PULL, o agente só descobre um desalinho se resolver perguntar, e o
|
|
233
|
+
* único hook instalado rodava DEPOIS de uma escrita - tarde demais para dizer "você não está
|
|
234
|
+
* logado" ou "este sistema não sabe de onde foi medido".
|
|
235
|
+
*/
|
|
236
|
+
session: want.hook
|
|
237
|
+
? await wireSessionStart(root, await alignCommand(root))
|
|
238
|
+
: "skipped",
|
|
214
239
|
mcp: want.mcp ? await wireMcp(root, version) : "skipped",
|
|
215
240
|
};
|
|
216
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* O hook `SessionStart`, escrito com a mesma disciplina do de escrita: vizinho nenhum é tocado, e um
|
|
244
|
+
* comando escrito à mão fica como está. Só um `align` nosso apontando para outro lugar é substituído.
|
|
245
|
+
*/
|
|
246
|
+
async function wireSessionStart(root, command) {
|
|
247
|
+
const dir = join(root, ".claude");
|
|
248
|
+
const path = join(dir, "settings.json");
|
|
249
|
+
const settings = await readJson(path);
|
|
250
|
+
const hooks = (settings.hooks ?? {});
|
|
251
|
+
const entries = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
252
|
+
for (const entry of entries)
|
|
253
|
+
for (const h of entry.hooks ?? []) {
|
|
254
|
+
const was = h.command ?? "";
|
|
255
|
+
if (!/synthesisui(@[\w.-]+)?\s+align/.test(was))
|
|
256
|
+
continue;
|
|
257
|
+
if (was === command)
|
|
258
|
+
return "already there";
|
|
259
|
+
h.command = command;
|
|
260
|
+
await mkdir(dir, { recursive: true });
|
|
261
|
+
await writeFile(path, `${JSON.stringify({ ...settings, hooks: { ...hooks, SessionStart: entries } }, null, 2)}\n`, "utf8");
|
|
262
|
+
return "updated";
|
|
263
|
+
}
|
|
264
|
+
entries.push({ hooks: [{ type: "command", command }] });
|
|
265
|
+
await mkdir(dir, { recursive: true });
|
|
266
|
+
await writeFile(path, `${JSON.stringify({ ...settings, hooks: { ...hooks, SessionStart: entries } }, null, 2)}\n`, "utf8");
|
|
267
|
+
return "added";
|
|
268
|
+
}
|
|
217
269
|
/**
|
|
218
270
|
* Whether THIS project already has the hook.
|
|
219
271
|
*
|
package/dist/commands/add.js
CHANGED
|
@@ -4,10 +4,43 @@ import { syncClaudeMd } from "../claude-md.js";
|
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
|
|
6
6
|
import { buildGuide } from "../guide.js";
|
|
7
|
+
import { censusScope } from "../measured-scope.js";
|
|
7
8
|
import { body as line, section, snippet } from "../output.js";
|
|
8
9
|
import { fetchDesignSystem } from "../registry.js";
|
|
9
10
|
import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.js";
|
|
10
11
|
import { detectStack } from "../stack.js";
|
|
12
|
+
/**
|
|
13
|
+
* QUAL METADE DESTA PASTA UM TIME COMMITA.
|
|
14
|
+
*
|
|
15
|
+
* Medido no repo do dono em 06/08: `_synthesisui/` não estava no gitignore E não tinha um único
|
|
16
|
+
* arquivo rastreado. Ou seja, a pasta existia só na máquina de quem rodou o import - todo clone, e
|
|
17
|
+
* todo colega, começava sem governança nenhuma, e nada dizia isso a ninguém.
|
|
18
|
+
*
|
|
19
|
+
* A divisão não é gosto, é o que cada arquivo É:
|
|
20
|
+
*
|
|
21
|
+
* .lock, ds/<slug>/** a IDENTIDADE e o CSS que a app importa. Sem eles o build de um colega
|
|
22
|
+
* não tem tokens. Versionar.
|
|
23
|
+
* census.json 0,5 MB, regravado inteiro a cada medição. Versionar é conflito toda vez.
|
|
24
|
+
* ledger.jsonl registro local, append-only, uma linha por escrita. Conflito garantido.
|
|
25
|
+
*
|
|
26
|
+
* Escrito uma vez e nunca reescrito: um arquivo que alguém ajustou é decisão dele.
|
|
27
|
+
*/
|
|
28
|
+
async function writeGovernanceIgnore(projectRoot) {
|
|
29
|
+
const path = join(projectRoot, "_synthesisui", ".gitignore");
|
|
30
|
+
try {
|
|
31
|
+
await access(path);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
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
|
+
"not-expressed.md\n", "utf8");
|
|
43
|
+
}
|
|
11
44
|
async function readRootLock(path) {
|
|
12
45
|
try {
|
|
13
46
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -62,14 +95,25 @@ export async function add(slug, opts) {
|
|
|
62
95
|
`@import "./v${payload.version}/${filename}";\n`, "utf8");
|
|
63
96
|
}
|
|
64
97
|
// 5. root pointer
|
|
98
|
+
/**
|
|
99
|
+
* A medição FRESCA vence a cópia anterior, e por isso lê o censo em vez do lock: um `--scope` novo
|
|
100
|
+
* chega pelo censo que acabou de rodar, e ler o lock aqui congelaria o primeiro para sempre. Sem
|
|
101
|
+
* censo (um `add` puro, sem import antes), o que o lock já dizia é preservado.
|
|
102
|
+
*/
|
|
103
|
+
const measured = await censusScope(projectRoot);
|
|
104
|
+
const scope = measured.system ?? prev?.scope ?? null;
|
|
105
|
+
const usage = measured.usage.length > 0 ? measured.usage : (prev?.usage ?? []);
|
|
65
106
|
const lock = {
|
|
66
107
|
slug: payload.slug,
|
|
67
108
|
name: payload.name,
|
|
68
109
|
version: payload.version,
|
|
69
110
|
registry: base,
|
|
70
111
|
fetchedAt: new Date().toISOString(),
|
|
112
|
+
...(scope ? { scope } : {}),
|
|
113
|
+
...(usage.length > 0 ? { usage } : {}),
|
|
71
114
|
};
|
|
72
115
|
await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
116
|
+
await writeGovernanceIgnore(projectRoot);
|
|
73
117
|
// 5b. governance rules (personal DS) → rules.md at the slug root (stable path,
|
|
74
118
|
// highest authority; the GUIDE tells the agent to read it first)
|
|
75
119
|
/**
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { readEvents } from "../doctor/ledger.js";
|
|
5
|
+
import { measuredScope } from "../measured-scope.js";
|
|
6
|
+
async function locksIn(root) {
|
|
7
|
+
const dsDir = join(root, "_synthesisui", "ds");
|
|
8
|
+
const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (!entry.isDirectory())
|
|
12
|
+
continue;
|
|
13
|
+
const raw = await readFile(join(dsDir, entry.name, ".lock"), "utf8").catch(() => null);
|
|
14
|
+
if (!raw)
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
out.push(JSON.parse(raw));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// lock ilegível - o `status` já reporta isso com o nome do arquivo
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
async function credentials(home) {
|
|
26
|
+
const raw = await readFile(join(home, ".synthesisui", "credentials.json"), "utf8").catch(() => null);
|
|
27
|
+
if (!raw)
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A metade que não depende de rede.
|
|
38
|
+
*
|
|
39
|
+
* Separada de propósito: ela é a que sempre roda, e é testável sem servidor nenhum.
|
|
40
|
+
*/
|
|
41
|
+
export async function localMisalignments(root,
|
|
42
|
+
/**
|
|
43
|
+
* `home` é injetável porque a SESSÃO mora nele e não no repo - e uma verificação que lê o home da
|
|
44
|
+
* máquina que roda o teste não é verificável: ela passa ou falha conforme quem está logado ali.
|
|
45
|
+
*/
|
|
46
|
+
opts = {}) {
|
|
47
|
+
const { cli, home = homedir() } = opts;
|
|
48
|
+
const out = [];
|
|
49
|
+
const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
|
|
50
|
+
/** Sem sistema instalado não há alinho a cobrar - não é desalinho, é um repo sem DS. */
|
|
51
|
+
if (locks.length === 0)
|
|
52
|
+
return out;
|
|
53
|
+
if (locks.length > 1)
|
|
54
|
+
out.push({
|
|
55
|
+
says: `${locks.length} design systems are installed here (${locks
|
|
56
|
+
.map((l) => l.slug)
|
|
57
|
+
.join(", ")}) - commands act on the first, so one of them is not being governed`,
|
|
58
|
+
});
|
|
59
|
+
const lock = locks[0];
|
|
60
|
+
const creds = await credentials(home);
|
|
61
|
+
if (!creds)
|
|
62
|
+
out.push({
|
|
63
|
+
says: "No session on this machine. A token belongs to the machine, not to the repo, so a fresh clone always lands here.",
|
|
64
|
+
run: "npx synthesisui login",
|
|
65
|
+
});
|
|
66
|
+
else if (creds.registry && lock.registry && creds.registry !== lock.registry)
|
|
67
|
+
out.push({
|
|
68
|
+
says: `Your session is for ${creds.registry} and this system came from ${lock.registry}. That answers 401, which reads as an expired session on a host that never issued one.`,
|
|
69
|
+
run: `npx synthesisui login --registry ${lock.registry}`,
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* O ESCOPO, que é o desalinho mais caro e o mais silencioso: sem ele o `sync` mede o repo inteiro
|
|
73
|
+
* e manda os componentes de todos os apps para um sistema que é uma biblioteca (06/08, 338 num
|
|
74
|
+
* sistema de 36).
|
|
75
|
+
*/
|
|
76
|
+
const scope = await measuredScope(root);
|
|
77
|
+
if (!scope.system && scope.usage.length === 0)
|
|
78
|
+
out.push({
|
|
79
|
+
says: `"${lock.slug}" does not record where it was measured, so a re-measure would read this whole repo instead of the folder the system came from.`,
|
|
80
|
+
run: "npx synthesisui import --dry --scope <folder>",
|
|
81
|
+
});
|
|
82
|
+
const events = await readEvents(root).catch(() => []);
|
|
83
|
+
if (events.length > 0)
|
|
84
|
+
out.push({
|
|
85
|
+
says: `${events.length} checked write${events.length === 1 ? "" : "s"} recorded here and not sent - the platform is scoring this repo on older evidence.`,
|
|
86
|
+
run: "npx synthesisui sync",
|
|
87
|
+
});
|
|
88
|
+
if (cli && lock.version == null)
|
|
89
|
+
out.push({
|
|
90
|
+
says: `"${lock.slug}" has no version recorded, so nothing can tell whether the CSS in this repo is the one the platform publishes.`,
|
|
91
|
+
run: "npx synthesisui upgrade",
|
|
92
|
+
});
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A linha que a sessão vê.
|
|
97
|
+
*
|
|
98
|
+
* Uma frase por desalinho e o comando ao lado - sem cabeçalho, sem contagem, sem moldura. Isto é
|
|
99
|
+
* injetado no contexto de um agente, e cada linha decorativa é contexto que não é sobre o trabalho
|
|
100
|
+
* dele.
|
|
101
|
+
*/
|
|
102
|
+
export function describeMisalignments(items) {
|
|
103
|
+
if (items.length === 0)
|
|
104
|
+
return "";
|
|
105
|
+
return [
|
|
106
|
+
"Before this session starts, this environment is out of alignment with the design system that governs it:",
|
|
107
|
+
...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `synthesisui align` - o comando que o hook de sessão roda.
|
|
112
|
+
*
|
|
113
|
+
* Sai silencioso e com código 0 quando está tudo alinhado, porque um hook que fala toda vez deixa de
|
|
114
|
+
* ser lido. E nunca falha: um ambiente que não dá para verificar não pode impedir alguém de
|
|
115
|
+
* trabalhar.
|
|
116
|
+
*/
|
|
117
|
+
export async function align(opts) {
|
|
118
|
+
const root = resolve(opts.dir ?? process.cwd());
|
|
119
|
+
const items = await localMisalignments(root, {
|
|
120
|
+
...(opts.cli ? { cli: opts.cli } : {}),
|
|
121
|
+
}).catch(() => []);
|
|
122
|
+
const text = describeMisalignments(items);
|
|
123
|
+
if (text)
|
|
124
|
+
console.log(text);
|
|
125
|
+
}
|
package/dist/commands/connect.js
CHANGED
|
@@ -50,6 +50,15 @@ export async function connect(opts) {
|
|
|
50
50
|
`✓ .claude/settings.json the check moved from ${wired.was?.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? "an older version"} to this one`
|
|
51
51
|
: "· .claude/settings.json already had it"));
|
|
52
52
|
console.log(snippet([wired.command]));
|
|
53
|
+
/**
|
|
54
|
+
* A SEGUNDA COSTURA, dita por nome. As onze ferramentas MCP são PULL e o hook de escrita roda
|
|
55
|
+
* DEPOIS de uma escrita - nenhum dos dois chega a tempo de dizer "você não está logado nesta
|
|
56
|
+
* máquina" ou "este sistema não sabe de onde foi medido". O `SessionStart` é o único que chega.
|
|
57
|
+
*/
|
|
58
|
+
if (wired.session !== "skipped")
|
|
59
|
+
console.log(body(wired.session === "already there"
|
|
60
|
+
? "· .claude/settings.json the session check was already there"
|
|
61
|
+
: `✓ .claude/settings.json every session now opens with what this environment is missing${wired.session === "updated" ? ", pointed at this version" : ""}`));
|
|
53
62
|
}
|
|
54
63
|
/**
|
|
55
64
|
* OS CAMINHOS DE MCP, POR NOME - e o número vem da LISTA, não da minha memória.
|
package/dist/commands/import.js
CHANGED
|
@@ -1674,6 +1674,22 @@ export async function takeCensus(root, opts) {
|
|
|
1674
1674
|
},
|
|
1675
1675
|
}
|
|
1676
1676
|
: {}),
|
|
1677
|
+
/**
|
|
1678
|
+
* O ESCOPO VIAJA COM O CENSO, e até 06/08 só o `usage` viajava.
|
|
1679
|
+
*
|
|
1680
|
+
* `census.scope` era escrito pelo comando `import` (o `runImport`, mais abaixo neste arquivo) e não
|
|
1681
|
+
* aqui - então todo censo produzido por uma RE-MEDIÇÃO nascia sem escopo, mesmo tendo lido a pasta
|
|
1682
|
+
* certa. Um veneno de ação lenta, medido no repo do dono:
|
|
1683
|
+
*
|
|
1684
|
+
* 1a re-medicao le o escopo guardado, mede `packages/ui` certo (36 looks),
|
|
1685
|
+
* e envia um censo SEM escopo
|
|
1686
|
+
* 2a re-medicao nao acha escopo -> mede o repo INTEIRO -> 340 looks, 324
|
|
1687
|
+
* componentes, e o rascunho curado de 37 vira 339
|
|
1688
|
+
*
|
|
1689
|
+
* A primeira parece perfeita, e é isso que faz o defeito sobreviver. O `usage` estava dois campos
|
|
1690
|
+
* acima o tempo todo, gravado por este mesmo objeto - os dois chegam juntos e só um sobrevivia.
|
|
1691
|
+
*/
|
|
1692
|
+
...(scopeLabel ? { scope: scopeLabel } : {}),
|
|
1677
1693
|
...((opts?.usage ?? []).length > 0
|
|
1678
1694
|
? { usage: (opts?.usage ?? []).map((u) => u.label) }
|
|
1679
1695
|
: {}),
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
3
|
import { join, relative, resolve } from "node:path";
|
|
3
4
|
import { pinnedHookVersion } from "../agent-wiring.js";
|
|
@@ -1057,4 +1058,20 @@ export async function mcp(opts) {
|
|
|
1057
1058
|
}
|
|
1058
1059
|
}
|
|
1059
1060
|
}
|
|
1060
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* A NOSSA VERSÃO, LIDA DO `package.json` - e ela era uma string cravada em "0.16.9".
|
|
1063
|
+
*
|
|
1064
|
+
* 149 versões atrás. O `index.ts` já lia a dele do arquivo desde sempre; esta cópia envelheceu em
|
|
1065
|
+
* silêncio e apareceu de três formas no mesmo dia (dono, 06/08):
|
|
1066
|
+
*
|
|
1067
|
+
* 1. `serverInfo.version` dizia 0.16.9, então o agente reportava "as ferramentas que estou segurando
|
|
1068
|
+
* são 0.16.9" - e estava repetindo o nosso número, não medindo nada
|
|
1069
|
+
* 2. a comparação nova (`pinned !== VERSION`) ficava SEMPRE verdadeira, então o "atualize o ds antes
|
|
1070
|
+
* de continuar" apareceria para sempre, inclusive um segundo depois de um `connect` bem-sucedido
|
|
1071
|
+
* 3. `remeasure({ cli: VERSION })` gravou `ledger.cli: "0.16.9"` no censo e no relatório dele, e a
|
|
1072
|
+
* plataforma passou a achar que aquele sistema está 149 versões atrás
|
|
1073
|
+
*
|
|
1074
|
+
* É a mesma lição que `MCP_TOOL_COUNT` já carrega neste arquivo - "um número que alguém digita
|
|
1075
|
+
* envelhece" - escrita duas vezes e aprendida uma.
|
|
1076
|
+
*/
|
|
1077
|
+
const VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
package/dist/commands/sync.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { readToken, resolveRegistry } from "../config.js";
|
|
4
4
|
import { readEvents } from "../doctor/ledger.js";
|
|
5
5
|
import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
6
|
+
import { measuredScope } from "../measured-scope.js";
|
|
6
7
|
import { body, paint, section, snippet } from "../output.js";
|
|
7
8
|
import { repoStateOf } from "../repo-state.js";
|
|
8
9
|
import { resolveReadParts, takeCensus } from "./import.js";
|
|
@@ -191,13 +192,31 @@ export async function remeasure(args) {
|
|
|
191
192
|
console.log(paint.blue(snippet(["npx synthesisui@latest connect"])));
|
|
192
193
|
console.log(body(paint.faint("That rewires the hook and the tools to the new version - the editor picks them up on the next session.")));
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* O ESCOPO SAI DO DISCO ANTES DE SAIR DO SERVIDOR.
|
|
197
|
+
*
|
|
198
|
+
* Ele mora nos dois lugares, e o do disco é o que não pode se perder: o censo local É a foto da
|
|
199
|
+
* última medição e carrega o `scope` que a mediu. O servidor guarda uma cópia, e uma cópia pode
|
|
200
|
+
* ser sobrescrita - em 06/08 foi, e o sistema ficou sem escopo nenhum lá.
|
|
201
|
+
*
|
|
202
|
+
* O preço daquilo não foi um número errado, foi um LOOP: sem escopo, esta linha media o repo
|
|
203
|
+
* inteiro, mandava 338 componentes de app para um sistema de 36, e o `sync` seguinte fazia de
|
|
204
|
+
* novo. Perguntar ao servidor uma coisa que está do lado de cá é o que fechava o ciclo.
|
|
205
|
+
*
|
|
206
|
+
* O servidor continua valendo como segunda fonte: um clone fresco tem o `.lock` e não tem censo.
|
|
207
|
+
*/
|
|
208
|
+
const local = await measuredScope(root);
|
|
209
|
+
const scope = local.system ?? stored.scope ?? null;
|
|
210
|
+
const usage = local.usage.length > 0 ? local.usage : (stored.usage ?? []);
|
|
194
211
|
console.log(section("Measuring your repository again"));
|
|
195
|
-
console.log(body(paint.faint(`${
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
console.log(body(paint.faint(`${scope ? `scope ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
|
|
213
|
+
? " · read from your own census, which is where the last measurement recorded it"
|
|
214
|
+
: ""} · the reading you already authored travels unchanged`)));
|
|
215
|
+
const census = await takeCensus(scope ? join(root, scope) : root, {
|
|
216
|
+
...(scope ? { scopeLabel: scope } : {}),
|
|
198
217
|
...(args.cli ? { cli: args.cli } : {}),
|
|
199
218
|
/** O `--usage` é EVIDÊNCIA e leva rótulo: sem ele o censo não sabe de que app o número veio. */
|
|
200
|
-
usage:
|
|
219
|
+
usage: usage.map((u) => ({
|
|
201
220
|
path: join(root, u),
|
|
202
221
|
label: u,
|
|
203
222
|
})),
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { absorb } from "./commands/absorb.js";
|
|
|
4
4
|
import { add } from "./commands/add.js";
|
|
5
5
|
import { adopt } from "./commands/adopt.js";
|
|
6
6
|
import { advise } from "./commands/advise.js";
|
|
7
|
+
import { align } from "./commands/align.js";
|
|
7
8
|
import { ci } from "./commands/ci.js";
|
|
8
9
|
import { clean } from "./commands/clean.js";
|
|
9
10
|
import { component } from "./commands/component.js";
|
|
@@ -547,6 +548,13 @@ async function main() {
|
|
|
547
548
|
yes: args.includes("--yes"),
|
|
548
549
|
});
|
|
549
550
|
break;
|
|
551
|
+
/**
|
|
552
|
+
* O que o hook `SessionStart` roda. Silencioso quando está alinhado, e nunca falha - ver
|
|
553
|
+
* `align.ts` para os nove estados e por que sete deles não tinham verificação nenhuma.
|
|
554
|
+
*/
|
|
555
|
+
case "align":
|
|
556
|
+
await align({ dir, cli: CLI_VERSION });
|
|
557
|
+
break;
|
|
550
558
|
case "request": {
|
|
551
559
|
// `synthesisui request` lists; `request component --name x --for "..."`
|
|
552
560
|
// files; `request --done <id>` closes. The agent's front door is the MCP
|
package/dist/measured-scope.js
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
const EMPTY = { system: null, usage: [], from: "none" };
|
|
4
|
+
/**
|
|
5
|
+
* O ESCOPO NO PONTEIRO DO SISTEMA, que é onde ele não se perde.
|
|
6
|
+
*
|
|
7
|
+
* O censo é grande (0,5 MB), é regravado a cada medição e ninguém versiona um arquivo assim sem
|
|
8
|
+
* pagar conflito. O `.lock` tem cinco campos e é o arquivo desta pasta que um time COMMITA - então é
|
|
9
|
+
* ele que faz um clone fresco saber de onde este sistema foi medido, sem uma ida à rede e sem
|
|
10
|
+
* depender de uma cópia no servidor que já foi sobrescrita uma vez (06/08).
|
|
11
|
+
*
|
|
12
|
+
* Um lock sem `scope` é o caso de todo sistema instalado antes disto: cai no censo, como antes.
|
|
13
|
+
*/
|
|
14
|
+
async function scopeInLock(root) {
|
|
15
|
+
const dsDir = join(root, "_synthesisui", "ds");
|
|
16
|
+
const names = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
|
|
17
|
+
for (const entry of names) {
|
|
18
|
+
if (!entry.isDirectory())
|
|
19
|
+
continue;
|
|
20
|
+
const raw = await readFile(join(dsDir, entry.name, ".lock"), "utf8").catch(() => null);
|
|
21
|
+
if (!raw)
|
|
22
|
+
continue;
|
|
23
|
+
let lock;
|
|
24
|
+
try {
|
|
25
|
+
lock = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const scope = typeof lock.scope === "string" && lock.scope.trim()
|
|
31
|
+
? lock.scope.trim()
|
|
32
|
+
: null;
|
|
33
|
+
const usage = Array.isArray(lock.usage)
|
|
34
|
+
? lock.usage.filter((u) => typeof u === "string" && !!u.trim())
|
|
35
|
+
: [];
|
|
36
|
+
if (!scope && usage.length === 0)
|
|
37
|
+
continue;
|
|
38
|
+
const system = scope && (await present(root, scope)) ? scope : null;
|
|
39
|
+
const kept = [];
|
|
40
|
+
for (const u of usage)
|
|
41
|
+
if (await present(root, u))
|
|
42
|
+
kept.push(u);
|
|
43
|
+
if (!system && kept.length === 0)
|
|
44
|
+
continue;
|
|
45
|
+
return { system, usage: kept, from: "lock" };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
4
49
|
/** Existe como diretório? Um escopo gravado e depois renomeado não pode derrubar o comando. */
|
|
5
50
|
async function present(root, rel) {
|
|
6
51
|
return stat(resolve(root, rel)).then((s) => s.isDirectory(), () => false);
|
|
@@ -13,6 +58,19 @@ async function present(root, rel) {
|
|
|
13
58
|
* comportamento antigo (a raiz).
|
|
14
59
|
*/
|
|
15
60
|
export async function measuredScope(root) {
|
|
61
|
+
const fromLock = await scopeInLock(root);
|
|
62
|
+
if (fromLock)
|
|
63
|
+
return fromLock;
|
|
64
|
+
return censusScope(root);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* O que o CENSO diz sobre onde, sem olhar o lock.
|
|
68
|
+
*
|
|
69
|
+
* Quem ESCREVE o lock lê por aqui, e a ordem é o contrário da leitura de propósito: o censo é a
|
|
70
|
+
* medição fresca que acabou de rodar, e o lock é a cópia da anterior. Ler o lock para escrever o
|
|
71
|
+
* lock congelaria o primeiro escopo para sempre, e um `--scope` novo nunca chegaria lá.
|
|
72
|
+
*/
|
|
73
|
+
export async function censusScope(root) {
|
|
16
74
|
const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => null);
|
|
17
75
|
if (!raw)
|
|
18
76
|
return EMPTY;
|
package/package.json
CHANGED