synthesisui 0.16.318 → 0.16.319

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.
@@ -308,6 +308,8 @@ export async function add(slug, opts) {
308
308
  const reference = lockReference(payload, prev);
309
309
  return reference ? { reference } : {};
310
310
  })(),
311
+ /** A memória do que NÓS escrevemos atravessa a re-materialização - ver `RootLock.written`. */
312
+ ...(prev?.written ? { written: prev.written } : {}),
311
313
  };
312
314
  const landed = (() => {
313
315
  if (!prev?.fetchedAt)
@@ -11,6 +11,7 @@ import { fetchComponent, RegistryError } from "../registry.js";
11
11
  import { flavourResolver } from "../styles-flavour.js";
12
12
  import { inTheirTongue, projectTongue } from "../their-tongue.js";
13
13
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
14
+ import { recordWritten } from "../written.js";
14
15
  /**
15
16
  * Writes the shared `cn.ts` next to the components, built from THIS project's
16
17
  * installed theme.
@@ -219,10 +220,16 @@ export async function component(slug, name, opts) {
219
220
  // it wears (.css) + the barrel. Ignores the css|tailwind flavor - the
220
221
  // template drives itself off the .ds-* classes.
221
222
  const tsx = interactiveTemplate(res.name);
222
- await writeFile(join(compDir, `${local}.tsx`), tsx, "utf8");
223
- await writeFile(join(compDir, `${local}.css`), `${css}\n`, "utf8");
224
- await writeFile(join(compDir, "index.ts"), `export * from "./${local}";\n`, "utf8");
225
- filenames = [`${local}.tsx`, `${local}.css`, "index.ts"];
223
+ const written = [
224
+ { filename: `${local}.tsx`, content: tsx },
225
+ { filename: `${local}.css`, content: `${css}\n` },
226
+ { filename: "index.ts", content: `export * from "./${local}";\n` },
227
+ ];
228
+ for (const f of written)
229
+ await writeFile(join(compDir, f.filename), f.content, "utf8");
230
+ /** O fingerprint do que escrevemos - é o que protege a edição dele no upgrade (T7). */
231
+ await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
232
+ filenames = written.map((f) => f.filename);
226
233
  }
227
234
  else {
228
235
  const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, flavour, await reactMajorOf(root),
@@ -258,12 +265,19 @@ export async function component(slug, name, opts) {
258
265
  * sistema não tem censo nenhum, e ele recebe o que sempre recebeu.
259
266
  */
260
267
  const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
261
- for (const file of files) {
262
- await writeFile(join(compDir, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
263
- }
268
+ /** Os bytes finais, para o fingerprint lembrar EXATAMENTE o que foi a disco (T7). */
269
+ const written = files.map((file) => ({
270
+ filename: file.filename,
271
+ content: file.filename.endsWith(".tsx")
272
+ ? `${nota}${file.code}`
273
+ : file.code,
274
+ }));
275
+ for (const f of written)
276
+ await writeFile(join(compDir, f.filename), f.content, "utf8");
277
+ await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
264
278
  if (flavour === "tailwind")
265
279
  await writeCn(root, compDir, slug);
266
- filenames = files.map((f) => f.filename);
280
+ filenames = written.map((f) => f.filename);
267
281
  }
268
282
  const flavor = wantInteractive ? "interactive" : `styles: ${flavour}`;
269
283
  console.log(`✓ ${config.componentsDir}/${local}/ → ${filenames.join(", ")} (${flavor})${local === res.name ? "" : ` - the "${res.name}" blueprint, under your name`}`);
@@ -6,6 +6,7 @@ import { installedSlugs } from "../installed.js";
6
6
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
7
7
  import { postGenerate, RegistryError } from "../registry.js";
8
8
  import { flavourResolver } from "../styles-flavour.js";
9
+ import { recordWritten } from "../written.js";
9
10
  /** PascalCase para o hint de import (course-card → CourseCard). */
10
11
  function pascalName(name) {
11
12
  return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
@@ -75,6 +76,8 @@ export async function generate(description, opts) {
75
76
  for (const file of files) {
76
77
  await writeFile(join(compDir, file.filename), file.code, "utf8");
77
78
  }
79
+ /** O fingerprint do que escrevemos - protege a edição dele no upgrade (T7). */
80
+ await recordWritten(join(root, "_synthesisui", "ds", slug), res.name, files.map((x) => ({ filename: x.filename, content: x.code })));
78
81
  materialized = true;
79
82
  console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
80
83
  }
@@ -7,6 +7,7 @@ import { body, section, snippet } from "../output.js";
7
7
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
8
8
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
9
9
  import { flavourResolver } from "../styles-flavour.js";
10
+ import { recordWritten } from "../written.js";
10
11
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
11
12
  * a folder holding only refit artifacts doesn't count). */
12
13
  /** True when the system is actually installed (tokens.css present). */
@@ -121,6 +122,8 @@ export async function refit(file, opts) {
121
122
  for (const f of files) {
122
123
  await writeFile(join(compDir, f.filename), f.code, "utf8");
123
124
  }
125
+ /** O fingerprint do que escrevemos - protege a edição dele no upgrade (T7). */
126
+ await recordWritten(join(root, "_synthesisui", "ds", slug), res.name, files.map((x) => ({ filename: x.filename, content: x.code })));
124
127
  materialized = true;
125
128
  console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
126
129
  }
@@ -11,6 +11,7 @@ import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../
11
11
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
12
  import { flavourResolver } from "../styles-flavour.js";
13
13
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
14
+ import { editedSinceWritten, keptLine, readWritten, recordWritten, } from "../written.js";
14
15
  import { add } from "./add.js";
15
16
  import { reportWhatIsLeft } from "./align.js";
16
17
  import { doctor } from "./doctor.js";
@@ -283,6 +284,9 @@ export async function upgrade(asked, opts) {
283
284
  const config = await readProjectConfig(root);
284
285
  const regenerated = [];
285
286
  const failed = [];
287
+ /** Componentes que ELE editou desde que os escrevemos - mantidos intactos (T7). */
288
+ const kept = [];
289
+ const writtenMap = await readWritten(slugDir);
286
290
  if (config.target === "next") {
287
291
  const componentsRoot = join(root, config.componentsDir);
288
292
  const marker = `from the "${slug}" design system`;
@@ -308,6 +312,17 @@ export async function upgrade(asked, opts) {
308
312
  }
309
313
  if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
310
314
  continue;
315
+ /**
316
+ * A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `written.ts`. O fingerprint do que NÓS
317
+ * escrevemos foi gravado no `.lock` na hora da escrita; se o disco divergiu, ele editou, e o
318
+ * arquivo é DELE - fica intacto e o terminal diz qual. `null` é install de antes do
319
+ * fingerprint: sem memória não há veredito, e o comportamento de sempre continua.
320
+ */
321
+ const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
322
+ if (edited && edited.length > 0) {
323
+ kept.push({ entry, edited });
324
+ continue;
325
+ }
311
326
  try {
312
327
  const res = await fetchComponent(base, slug, entry);
313
328
  const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
@@ -325,9 +340,17 @@ export async function upgrade(asked, opts) {
325
340
  * Ver `unread-for-component.ts`: censo ausente responde vazio, e vazio não vira comentário.
326
341
  */
327
342
  const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
328
- for (const file of files) {
329
- await writeFile(join(componentsRoot, entry, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
343
+ /** Os bytes finais, para o fingerprint lembrar EXATAMENTE o que foi a disco. */
344
+ const finalFiles = files.map((file) => ({
345
+ filename: file.filename,
346
+ content: file.filename.endsWith(".tsx")
347
+ ? `${nota}${file.code}`
348
+ : file.code,
349
+ }));
350
+ for (const file of finalFiles) {
351
+ await writeFile(join(componentsRoot, entry, file.filename), file.content, "utf8");
330
352
  }
353
+ await recordWritten(slugDir, entry, finalFiles);
331
354
  regenerated.push(entry);
332
355
  }
333
356
  catch {
@@ -372,6 +395,13 @@ export async function upgrade(asked, opts) {
372
395
  .map((n) => `\`${n}\``)
373
396
  .join(", ")}.`
374
397
  : "- No materialized components needed regeneration.",
398
+ ...(kept.length > 0
399
+ ? [
400
+ `- These components were edited by hand after we wrote them, so the upgrade KEPT your files: ${kept
401
+ .map((k) => `\`${k.entry}\``)
402
+ .join(", ")}. To take the new version of one, rerun \`npx synthesisui component ${slug} <name>\`.`,
403
+ ]
404
+ : []),
375
405
  "- The full new contract lives in `design-system.json` / `GUIDE.md` next to this file.",
376
406
  "",
377
407
  ].join("\n");
@@ -394,6 +424,12 @@ export async function upgrade(asked, opts) {
394
424
  console.log("");
395
425
  console.log(body(`⚠ Could not regenerate: ${failed.join(", ")} (removed/renamed in v${latest.version}?)`));
396
426
  }
427
+ /** A prova VISÍVEL do T7: o dono fica sabendo AGORA, com o arquivo nomeado e a saída dita. */
428
+ if (kept.length > 0) {
429
+ console.log("");
430
+ for (const k of kept)
431
+ console.log(body(keptLine(slug, k.entry, k.edited)));
432
+ }
397
433
  await checkAfterWriting(root, `v${latest.version}`);
398
434
  console.log(section("Migrate the app"));
399
435
  console.log(body(`The migration brief is at _synthesisui/ds/${slug}/UPGRADE.md`));
@@ -137,7 +137,14 @@
137
137
  * ali, que é exatamente o que esta marca existe para dizer. Quem avisa versão nova passa a ser o
138
138
  * próprio align, com o `cli` que o servidor manda no `?meta=1`.
139
139
  */
140
- export const MATERIALISER_SINCE = "0.16.306";
140
+ /**
141
+ * 0.16.306 -> 0.16.319 em 26/08 (T7), e o SIM é sobre DUAS coisas que caem na pasta: o `.lock`
142
+ * ganha `written` (o fingerprint do que NÓS escrevemos, por componente e arquivo) e o `upgrade`
143
+ * muda QUAIS bytes reescreve - um componente que o dono editou à mão fica INTACTO, com a frase no
144
+ * terminal dizendo qual arquivo e como tomar a versão nova por escolha. Antes desta versão a
145
+ * reescrita era incondicional e o único aviso era prosa depois do fato.
146
+ */
147
+ export const MATERIALISER_SINCE = "0.16.319";
141
148
  /**
142
149
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
143
150
  *
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ export function fingerprintOf(content) {
5
+ return createHash("sha256")
6
+ .update(content, "utf8")
7
+ .digest("hex")
8
+ .slice(0, 12);
9
+ }
10
+ /** O mapa gravado, ou vazio - um `.lock` ausente/ilegível responde como install legado. */
11
+ export async function readWritten(slugDir) {
12
+ try {
13
+ const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8"));
14
+ return lock.written ?? {};
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ /**
21
+ * Grava o fingerprint do que acabou de ser escrito. Lê-modifica-escreve o `.lock` inteiro para
22
+ * não perder nenhum campo irmão; sem `.lock` (componente materializado antes do install), não há
23
+ * onde lembrar - silêncio de propósito, o próximo `add` cria o arquivo e o próximo write grava.
24
+ */
25
+ export async function recordWritten(slugDir, entry, files) {
26
+ const lockPath = join(slugDir, ".lock");
27
+ let lock;
28
+ try {
29
+ lock = JSON.parse(await readFile(lockPath, "utf8"));
30
+ }
31
+ catch {
32
+ return;
33
+ }
34
+ const written = (lock.written ?? {});
35
+ written[entry] = Object.fromEntries(files.map((f) => [f.filename, fingerprintOf(f.content)]));
36
+ lock.written = written;
37
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
38
+ }
39
+ /**
40
+ * QUAIS ARQUIVOS ELE EDITOU desde que nós os escrevemos.
41
+ *
42
+ * Um arquivo que sumiu também conta: apagar é uma edição - regenerá-lo desfaria a decisão dele
43
+ * do mesmo jeito. Devolve vazio quando nada divergiu (nosso para regenerar) e `null` quando não
44
+ * há fingerprint gravado (install legado - o chamador mantém o comportamento de sempre).
45
+ */
46
+ export async function editedSinceWritten(recorded, dir) {
47
+ if (!recorded)
48
+ return null;
49
+ const edited = [];
50
+ for (const [filename, fp] of Object.entries(recorded)) {
51
+ try {
52
+ const current = await readFile(join(dir, filename), "utf8");
53
+ if (fingerprintOf(current) !== fp)
54
+ edited.push(filename);
55
+ }
56
+ catch {
57
+ edited.push(filename);
58
+ }
59
+ }
60
+ return edited;
61
+ }
62
+ /**
63
+ * A FRASE DO TERMINAL, uma por componente mantido - o critério do T7 é a prova VISÍVEL: o dono
64
+ * fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato. A
65
+ * saída oferecida regenera por ESCOLHA (o `component` reescreve e regrava o fingerprint).
66
+ */
67
+ export function keptLine(slug, entry, edited) {
68
+ return (`! ${entry}: you edited ${edited.join(", ")} after we wrote it - kept YOUR file(s).\n` +
69
+ ` To take the new version anyway: npx synthesisui component ${slug} ${entry}`);
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.318",
3
+ "version": "0.16.319",
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": {