synthesisui 0.16.10 → 0.16.11

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.
@@ -0,0 +1,76 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative, resolve } from "node:path";
3
+ import { diagnose, scanSource } from "../doctor/scan.js";
4
+ import { loadSystem } from "./doctor.js";
5
+ const pass = () => ({ continue: true });
6
+ const speak = (context) => ({
7
+ continue: true,
8
+ hookSpecificOutput: {
9
+ hookEventName: "PostToolUse",
10
+ additionalContext: context,
11
+ },
12
+ });
13
+ /** Files where a design value can even appear. A hook that parses a README on
14
+ * every edit is paying for nothing. */
15
+ const UI_FILE = /\.(tsx|jsx|ts|js|css|scss|vue|svelte)$/i;
16
+ async function report(root, filePath) {
17
+ const { table } = await loadSystem(root);
18
+ if (table.byName.size === 0)
19
+ return null;
20
+ const src = await readFile(filePath, "utf8").catch(() => "");
21
+ if (!src)
22
+ return null;
23
+ const rel = relative(root, filePath);
24
+ const d = diagnose([scanSource(rel, src, table)]);
25
+ const named = d.findings.filter((f) => f.token);
26
+ const phantoms = d.files.flatMap((f) => f.phantoms ?? []);
27
+ // Unnamed drift alone is deliberately NOT worth interrupting for. There is
28
+ // no token to move to, so the only honest advice is "ask a person" - and
29
+ // saying that after every edit trains the reader to skip the block.
30
+ if (named.length === 0 && phantoms.length === 0)
31
+ return null;
32
+ const lines = [`${rel} - checked against ${table.name ?? table.slug}.`];
33
+ if (named.length > 0) {
34
+ lines.push("", "Values written by hand that this system already has a name for:", ...named
35
+ .slice(0, 20)
36
+ .map((f) => ` line ${f.line} ${f.literal} → ${f.token}`), "", "Replace them now, while you still have this file in mind.");
37
+ }
38
+ if (phantoms.length > 0) {
39
+ lines.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms.slice(0, 20).map((p) => ` line ${p.line} ${p.name}`), "", "Use a name the system has, or say which value you need and what you would call it. Do NOT invent a token.");
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+ export async function hook(opts) {
44
+ const root = resolve(opts.dir ?? process.cwd());
45
+ let raw = "";
46
+ for await (const chunk of process.stdin)
47
+ raw += chunk;
48
+ let input;
49
+ try {
50
+ input = JSON.parse(raw);
51
+ }
52
+ catch {
53
+ // Anything we cannot read must let the edit through untouched. A hook that
54
+ // fails loudly on a payload change is worse than no hook.
55
+ process.stdout.write(`${JSON.stringify(pass())}\n`);
56
+ return;
57
+ }
58
+ const writes = input.tool_name === "Write" ||
59
+ input.tool_name === "Edit" ||
60
+ input.tool_name === "MultiEdit";
61
+ const file = input.tool_input?.file_path;
62
+ if (input.hook_event_name !== "PostToolUse" || !writes || !file) {
63
+ process.stdout.write(`${JSON.stringify(pass())}\n`);
64
+ return;
65
+ }
66
+ const abs = resolve(root, file);
67
+ // Our own installed artifacts are the answer, not the problem - and a file
68
+ // outside the project is none of our business.
69
+ const inside = !relative(root, abs).startsWith("..");
70
+ if (!UI_FILE.test(abs) || !inside || abs.includes("_synthesisui")) {
71
+ process.stdout.write(`${JSON.stringify(pass())}\n`);
72
+ return;
73
+ }
74
+ const body = await report(root, abs).catch(() => null);
75
+ process.stdout.write(`${JSON.stringify(body ? speak(body) : pass())}\n`);
76
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { clean } from "./commands/clean.js";
6
6
  import { component } from "./commands/component.js";
7
7
  import { doctor } from "./commands/doctor.js";
8
8
  import { generate } from "./commands/generate.js";
9
+ import { hook } from "./commands/hook.js";
9
10
  import { init } from "./commands/init.js";
10
11
  import { list } from "./commands/list.js";
11
12
  import { login } from "./commands/login.js";
@@ -134,6 +135,11 @@ async function main() {
134
135
  slug: typeof flags.slug === "string" ? flags.slug : undefined,
135
136
  });
136
137
  break;
138
+ // Both of these read stdin and write a protocol frame to stdout, so
139
+ // nothing that prints may run near them.
140
+ case "hook":
141
+ await hook({ dir });
142
+ return;
137
143
  // Long-lived: it owns stdin/stdout until the client closes the pipe, so
138
144
  // it must not be reached by anything that prints.
139
145
  case "mcp":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.10",
3
+ "version": "0.16.11",
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": {