mlola-ui 1.0.2 → 1.0.4

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.
Files changed (38) hide show
  1. package/README.md +30 -3
  2. package/package.json +1 -1
  3. package/registry/agents.json +419 -0
  4. package/registry/agents.md +453 -0
  5. package/registry/assets.json +734 -110
  6. package/registry/catalog.json +1187 -0
  7. package/registry/contract.json +1064 -0
  8. package/registry/index.json +65 -65
  9. package/registry/source/packages/components/alert/alert.tsx +1 -1
  10. package/registry/source/packages/components/badge/badge.tsx +2 -2
  11. package/registry/source/packages/components/breadcrumb/breadcrumb.tsx +1 -1
  12. package/registry/source/packages/components/carousel/carousel.tsx +1 -0
  13. package/registry/source/packages/components/checkbox/checkbox.tsx +3 -2
  14. package/registry/source/packages/components/color-picker/color-picker.tsx +15 -13
  15. package/registry/source/packages/components/color-picker/color.ts +1 -1
  16. package/registry/source/packages/components/combobox/combobox.tsx +33 -6
  17. package/registry/source/packages/components/date-picker/date-picker.tsx +5 -3
  18. package/registry/source/packages/components/dropzone/dropzone.tsx +1 -1
  19. package/registry/source/packages/components/empty-state/empty-state.tsx +1 -1
  20. package/registry/source/packages/components/hover-card/hover-card.tsx +1 -1
  21. package/registry/source/packages/components/number-input/number-input.tsx +6 -5
  22. package/registry/source/packages/components/otp-input/otp-input.tsx +6 -4
  23. package/registry/source/packages/components/progress/progress.tsx +1 -1
  24. package/registry/source/packages/components/segmented-control/segmented-control.tsx +5 -3
  25. package/registry/source/packages/components/select/select.tsx +6 -5
  26. package/registry/source/packages/components/tag-input/tag-input.tsx +7 -6
  27. package/registry/source/packages/components/textarea/textarea.tsx +1 -1
  28. package/registry/source/packages/components/time-picker/time-picker.tsx +8 -5
  29. package/registry/source/packages/components/timeline/timeline.tsx +1 -1
  30. package/registry/source/packages/components/toast/toast.tsx +1 -1
  31. package/registry/source/packages/components/toggle/toggle.tsx +1 -0
  32. package/registry/source/packages/components/tour/tour.tsx +3 -3
  33. package/registry/source-manifest.json +24 -24
  34. package/src/agents.js +119 -0
  35. package/src/cli.js +29 -4
  36. package/src/knowledge.js +235 -0
  37. package/src/mcp.js +241 -0
  38. package/src/pro.js +1 -1
@@ -8,43 +8,43 @@
8
8
  "packages/components/accordion/accordion.tsx": "sha256-0ALaLJd1fvj8Vbx86HLBKm3qUItrtwbrGJTykVfNSMg=",
9
9
  "packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-fMGDlXKpOfIOy0hmbpR8hKrbbN3fC386tXK27A21ZwM=",
10
10
  "packages/components/activity-heatmap/calendar.ts": "sha256-NB+AjXY5Xc+KzR8MMTRBgZxdzOeoHhkW0KQOW7WJPuQ=",
11
- "packages/components/alert/alert.tsx": "sha256-kg//PTcUW7j7AEnhfO/pZFddm++RIo8yKur46OeoeNo=",
11
+ "packages/components/alert/alert.tsx": "sha256-j680akXvCIh4MTVnUo79acvVrv51XyQ3UBJ5FBAy8Dw=",
12
12
  "packages/components/app-shell/app-shell.tsx": "sha256-XAq82XOTH3ZR+hxTkYFu8P7Ih2aI6ERy/TpXc2VMUwM=",
13
13
  "packages/components/avatar/avatar.tsx": "sha256-hvZUGhatN95ZyX6p6NBhTRf6h1/rrJ+bJrqBMtIspq0=",
14
- "packages/components/badge/badge.tsx": "sha256-+mgudoxlPn+mjpQycUfQmv8sC/e/K5FXfXm2+wnH7bI=",
15
- "packages/components/breadcrumb/breadcrumb.tsx": "sha256-SiIpg0oAieyoZbMlBwwQDZhR/KVIFLFCLCQJ6vwv7Fs=",
14
+ "packages/components/badge/badge.tsx": "sha256-flEH8S2jZ/64n3Y/ROhC9+9rvF7wXaN0xLcMMwqNOVQ=",
15
+ "packages/components/breadcrumb/breadcrumb.tsx": "sha256-Mt9mvmwwDgdp5/+mj5byIrh8+hicsp7R6UcvUlK0yog=",
16
16
  "packages/components/button/button.tsx": "sha256-khqYxcV3hlZ+Q+cYNTRXEpAfIijDFSvkuDJXMPtlk7g=",
17
17
  "packages/components/calendar/calendar.tsx": "sha256-uE22LI4jpU8ErR/oBmAkeKiu+Rp0bz3IXZVCurycpwc=",
18
18
  "packages/components/calendar/dates.ts": "sha256-mygYkoC70av2ZQ0vkxf885trwW97Wd10I4dptYQe+4A=",
19
19
  "packages/components/card/card.tsx": "sha256-aMKbv5DWYYWq7GZAFR9O1fuYE0H61WCjkvEbRMOd894=",
20
- "packages/components/carousel/carousel.tsx": "sha256-LAPKz9DG9vEBv/8jKjqgH7XiSEERkWWLr4n/Kve1omQ=",
21
- "packages/components/checkbox/checkbox.tsx": "sha256-cqTFwYwNPRxKiBnXXWkcFeDlJ6tpJ9GX4IdqrYRp9Fs=",
22
- "packages/components/color-picker/color-picker.tsx": "sha256-UziuULSI7dIGLfu4jjWIz6Cbh2/HiaY2KYXlfQ+6iQg=",
23
- "packages/components/color-picker/color.ts": "sha256-3IO6DaelZ6TVn2aw+6o6pZCQGAQdeNRkN7sPypUzddE=",
24
- "packages/components/combobox/combobox.tsx": "sha256-CnuJP01m0ZPMB4MGjoKbJCBQ7inI6bPf5aMDrq4m1qI=",
20
+ "packages/components/carousel/carousel.tsx": "sha256-pamugpB6Ujt9Ylxd37jNF/ZwSb8iRkW/MoYe12nY3i4=",
21
+ "packages/components/checkbox/checkbox.tsx": "sha256-hz4YbGwOxv/aJpJHrLXR4MKSWJGyjzoJEIGvdhyML9M=",
22
+ "packages/components/color-picker/color-picker.tsx": "sha256-E+QB6MbSxg+3AZX3A7bOkst4arxgpmFL2ZmochoWCl8=",
23
+ "packages/components/color-picker/color.ts": "sha256-YqSgYjzn7XfPWAHuyzbwOhbg/ScaSfB7V73e9w+OjJs=",
24
+ "packages/components/combobox/combobox.tsx": "sha256-CvuYc/hJoS7yw6QM9vv1SIkwflx0Kwd95UjchtC+AOs=",
25
25
  "packages/components/command-menu/command-menu.tsx": "sha256-mF1z92yrPY609pjx5Dt1l5EhGHl9EWV8F8Sq0hyJRw4=",
26
26
  "packages/components/context-menu/context-menu.tsx": "sha256-YgYsIzvLGFcJG1XX5zFJwrid5Z1Kn0lyWVzmLKmbMVU=",
27
- "packages/components/date-picker/date-picker.tsx": "sha256-pX7snZCO3SxVftkCvI/f5b82bNHPconUa50BJnjYNWc=",
27
+ "packages/components/date-picker/date-picker.tsx": "sha256-rsmDweD3RVfKTXR2i+/pnTb/MsrDAZXmHAYBvcUGxig=",
28
28
  "packages/components/dropdown-menu/dropdown-menu.tsx": "sha256-xgybCu9HBxIieafu5CjAtY4aCOQoaAj/VUCH6D6vLP8=",
29
- "packages/components/dropzone/dropzone.tsx": "sha256-jMVEnTki4Suhx7pOWfLX9MO1Q3nLz0xqDJ/2EPHdpYg=",
30
- "packages/components/empty-state/empty-state.tsx": "sha256-AWwOuZgm71hossjCGYygtvzGcDy6JOPryF+U1UrsLhM=",
31
- "packages/components/hover-card/hover-card.tsx": "sha256-HZyB9SUDcRPG8MvRX1sHqHA2rNcO63yVevH9hiGqVAA=",
29
+ "packages/components/dropzone/dropzone.tsx": "sha256-tHCCpEWE6839UeLOszTo+R2vAL4Sp9UxuRdLkFLfnq4=",
30
+ "packages/components/empty-state/empty-state.tsx": "sha256-uCKqYpr9vkozXZHpdl4GFtdX9ijcGOriCHvgyarBqRw=",
31
+ "packages/components/hover-card/hover-card.tsx": "sha256-AyRbxfqDOYBgGlgjgrtQZm5DipArLEzQn6XnzjPd0ro=",
32
32
  "packages/components/input/input.tsx": "sha256-lbbheo+ydQTzyujTf0W7DD96TogOjifCyqj9c3h3W0g=",
33
33
  "packages/components/kbd/kbd.tsx": "sha256-wrPqzn1e82zJYoKzO7u94Bgq4+nPc0DXLc8GJ/4/3nY=",
34
34
  "packages/components/modal/modal.tsx": "sha256-vybxgBGYCrMYvJRnO1wDAc2VFQylu+mQIf5OZIcabX4=",
35
- "packages/components/number-input/number-input.tsx": "sha256-D+8jH2YaW4lCv/F02qvkc9Huhxvz0y1idzgOBwsjiN8=",
35
+ "packages/components/number-input/number-input.tsx": "sha256-BDOyvCNtMULD/HK7uWHsYbGg79lrRDrVqtGQQrYHpmM=",
36
36
  "packages/components/number-input/number.ts": "sha256-IrHCQrEhI5ein9ifMFuSViSoxQUBoT3tNmmCu/paGGM=",
37
- "packages/components/otp-input/otp-input.tsx": "sha256-MWDyTP99wVAQSYJsF78ol1Kv9LKOj1lybmp2zOomVqo=",
37
+ "packages/components/otp-input/otp-input.tsx": "sha256-QURn8g5HqCQXBTdeENPbVxoPlqs3ZtoAPLRlFjQDxmI=",
38
38
  "packages/components/pagination/pagination.tsx": "sha256-FBwk8tq4JcsK2D0dlWEYiDSRx3owgBmGzEIlVRaNBoE=",
39
39
  "packages/components/popover/popover.tsx": "sha256-/kTiAuFklU4IuP97XaiCUGdEpKtNyMPLPS359mfzPHw=",
40
40
  "packages/components/priority-icon/labels.ts": "sha256-zhmJIb7/gS16wU+xCJ7hkReBi9q5RI3SMwdjkJvvvKU=",
41
41
  "packages/components/priority-icon/priority-icon.tsx": "sha256-3D6hFRpht3YuWouLhakE2DFdldavmJx0B/povEDwrtQ=",
42
- "packages/components/progress/progress.tsx": "sha256-3SOA1JX54msiq/pF2UolesQqQZm0zSVyZQi6W38lOOA=",
42
+ "packages/components/progress/progress.tsx": "sha256-9OjecRNPWn7Bfe+C5IyXaKsW1NJae6X5gco1o0X5XkE=",
43
43
  "packages/components/radio-group/radio-group.tsx": "sha256-jhnSEBLkdpe8dv5us/Q1sMsQF4sKGRgDXWGKm2JSFew=",
44
44
  "packages/components/rating/rating.tsx": "sha256-zgPOQXzAiRE4qGNPQ8qat3SP1XSI/7567kA5VaT51P0=",
45
45
  "packages/components/resizable/resizable.tsx": "sha256-40sK2UyIR9Ek72KAu5nvbr44tKEY6JhC5Ftb++Wf6cI=",
46
- "packages/components/segmented-control/segmented-control.tsx": "sha256-sExMN2WHtzxcaCu7xN/1jaP+sRVwJ2OAW1WkKGhNYnU=",
47
- "packages/components/select/select.tsx": "sha256-MWHL/mlAO2ob4H3EmYSBZcC1qqy+M3cF1Z4s0R+frrc=",
46
+ "packages/components/segmented-control/segmented-control.tsx": "sha256-MCBG5XHFWCwbACctsYKbqe8mvSSR6pf60tyRgdCDKDA=",
47
+ "packages/components/select/select.tsx": "sha256-m8QmKUGC4386XcklosSntal0XwUAFjKosTBQSF0dduQ=",
48
48
  "packages/components/sheet/sheet.tsx": "sha256-EIn3OeRDmXGHbYf/uihRh8aw3LmzarIFiyC9DUsL1jg=",
49
49
  "packages/components/skeleton/skeleton.tsx": "sha256-2Fk72NbLr3+CrXH7jGbHNsc3t+vskMqSyySe/UwbGCs=",
50
50
  "packages/components/slider/slider.tsx": "sha256-l+VrBqPYHIIcemiVixxwDxNDB0HEMmYRZekx/H5HroQ=",
@@ -53,15 +53,15 @@
53
53
  "packages/components/stepper/stepper.tsx": "sha256-uI/dRmkbijlixHiGUceWcpaDzpEixrzQ1UTmoPbGZAY=",
54
54
  "packages/components/table/table.tsx": "sha256-ed1Qn2sIpxRvGdPDAb5ih5AcXr5PmHTHdlrJLYDYRp8=",
55
55
  "packages/components/tabs/tabs.tsx": "sha256-kWAQjE9mshqZL7V3NJTUiurp6M5Nu3vx5O1LsIgdV5Y=",
56
- "packages/components/tag-input/tag-input.tsx": "sha256-+R1f2XljDYInsfuggKkpqz5m9TqEa0g3QYKMX2TE6h0=",
57
- "packages/components/textarea/textarea.tsx": "sha256-jeyvAlzFr25eWZ/Inkxo1UEOHXlCPB/QHeId124SfR8=",
58
- "packages/components/time-picker/time-picker.tsx": "sha256-OyIfJN6xVv6TwUYtRirkemJe5VMcu2HXxjaJj8fvu9I=",
56
+ "packages/components/tag-input/tag-input.tsx": "sha256-EK7xJTe3Y/S5Kn3vS0zBQg9lC+RVoGvky6HOAt+DnAw=",
57
+ "packages/components/textarea/textarea.tsx": "sha256-C9jMYZ63w+m6bgdLvHjvULRuoCe26q5zbgS4+ahWzr8=",
58
+ "packages/components/time-picker/time-picker.tsx": "sha256-og8Cbf+e8oFYewfagUgjRzLHSIIlAZh+1OpAn9yNq70=",
59
59
  "packages/components/time-picker/times.ts": "sha256-V/4uqFZPn9gVnyx8cB9vwp/RocmTYaPmLxjfy8DPQOI=",
60
- "packages/components/timeline/timeline.tsx": "sha256-FDZx+ihL5tbIY/sEl0444uj27JWHE5xl0vl8IqRUUPY=",
61
- "packages/components/toast/toast.tsx": "sha256-RrqUAAfKavEkZaLjywJHc0iTNHC1DHlIGVX97kftDkg=",
62
- "packages/components/toggle/toggle.tsx": "sha256-zwTbHMSs3n0eg5XIpREDnySYLXHaLly1Ew4FWlzvgJY=",
60
+ "packages/components/timeline/timeline.tsx": "sha256-nVg7lBcAUAzqEb0YrHagJXSaHE2aWWE9bUbQMWQY6PU=",
61
+ "packages/components/toast/toast.tsx": "sha256-FvZnwN6Lk/QEHmcNxo7sVlEvR3Lx54aT5cH9lwrthhE=",
62
+ "packages/components/toggle/toggle.tsx": "sha256-8qPHeSliHNdzQS7OLJ47BrvRR75MYyUGJlK4s21XS/U=",
63
63
  "packages/components/tooltip/tooltip.tsx": "sha256-NbaOUmlUbBlUIdovchrWwH2QqzuvXB97DjnJnrHf4Gc=",
64
- "packages/components/tour/tour.tsx": "sha256-1aSh7hMVt/DM/kH3WGJzJMGNsNqYEOmnwAZwc8dwRv8=",
64
+ "packages/components/tour/tour.tsx": "sha256-mrAE8aVDBVIibGvQah9Gh3mzIiWDFxB6aObzqFo5xXU=",
65
65
  "packages/components/virtual-list/virtual-list.tsx": "sha256-raGEFn4HlAOIohqZcy0DgptsnUIPS/6bWLYeTe+M9E8=",
66
66
  "packages/components/virtual-list/virtual.ts": "sha256-PyDIj3C2Vwb1i4ZssxSlYjgKmZPBIFLZFNq8YCW2Z78="
67
67
  }
package/src/agents.js ADDED
@@ -0,0 +1,119 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { designGuide } from "./knowledge.js";
4
+
5
+ /**
6
+ * Tells the project's coding agents that the UI is Mlola, where the design
7
+ * language is written down, and how to ask about it, so an assistant builds
8
+ * with Mlola without being reminded:
9
+ *
10
+ * - `mlola.agents.md` the design guide: classes, tokens, rules (refreshed)
11
+ * - `AGENTS.md` a short section, between markers, read by most agents
12
+ * - `CLAUDE.md` imports AGENTS.md for Claude Code
13
+ * - `.mcp.json` the Mlola MCP server, for Claude Code and others
14
+ * - `.cursor/…` a rule and the MCP server, when the project uses Cursor
15
+ * - `.vscode/mcp.json` the MCP server, when the project uses VS Code
16
+ *
17
+ * Every write merges: anything else in these files is left as it was.
18
+ */
19
+
20
+ export const GUIDE_FILE = "mlola.agents.md";
21
+ const START = "<!-- mlola-ui:start -->";
22
+ const END = "<!-- mlola-ui:end -->";
23
+ const SERVER = { command: "npx", args: ["-y", "mlola-ui", "mcp"] };
24
+
25
+ const BRIEF = `## UI: Mlola UI
26
+
27
+ This project's interface is built with Mlola UI. Before writing or changing UI:
28
+
29
+ - Read \`${GUIDE_FILE}\` (and \`mlola-pro.agents.md\` when it exists): every
30
+ class, attribute and token, and the rules for new UI.
31
+ - Compose from Mlola components first: \`npx mlola-ui list\`,
32
+ \`npx mlola-ui add <name>\`. Write new CSS only for what they do not cover.
33
+ - Never write a color, spacing, radius, shadow or duration by hand: read the
34
+ \`--ml-*\` tokens. There is no Tailwind or other styling framework here.
35
+ - State and variants go in \`data-*\` and \`aria-*\` with the shared words
36
+ (\`data-tone\`, \`data-variant\`, \`data-size\`, \`data-status\`), never in extra
37
+ classes. \`data-theme\` and \`data-mode\` belong to the engine.
38
+ - The \`mlola\` MCP server (\`npx mlola-ui mcp\`) searches components, explains
39
+ tokens and checks markup against the contract. Check new markup with its
40
+ \`check_markup\` tool before finishing.`;
41
+
42
+ /** Replaces the marked section, or appends it; returns what happened. */
43
+ function mergeSection(filename, body) {
44
+ const block = `${START}\n${body}\n${END}`;
45
+ if (!fs.existsSync(filename)) {
46
+ fs.writeFileSync(filename, `${block}\n`);
47
+ return "created";
48
+ }
49
+ const current = fs.readFileSync(filename, "utf8");
50
+ const start = current.indexOf(START);
51
+ const end = current.indexOf(END);
52
+ const next = start >= 0 && end > start ? `${current.slice(0, start)}${block}${current.slice(end + END.length)}` : `${current.replace(/\s*$/, "")}\n\n${block}\n`;
53
+ if (next === current) return "unchanged";
54
+ fs.writeFileSync(filename, next);
55
+ return "updated";
56
+ }
57
+
58
+ function mergeJson(filename, update) {
59
+ let data = {};
60
+ if (fs.existsSync(filename)) {
61
+ try {
62
+ data = JSON.parse(fs.readFileSync(filename, "utf8"));
63
+ } catch {
64
+ return "skipped (not valid JSON; add the server by hand)";
65
+ }
66
+ }
67
+ const next = update(structuredClone(data));
68
+ if (JSON.stringify(next) === JSON.stringify(data)) return "unchanged";
69
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
70
+ const existed = fs.existsSync(filename);
71
+ fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`);
72
+ return existed ? "updated" : "created";
73
+ }
74
+
75
+ export function writeAgentFiles(cwd) {
76
+ const results = [];
77
+ const record = (file, status) => results.push({ file, status });
78
+
79
+ const guide = designGuide();
80
+ if (guide) {
81
+ const filename = path.join(cwd, GUIDE_FILE);
82
+ const before = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : null;
83
+ if (before !== guide) fs.writeFileSync(filename, guide);
84
+ record(GUIDE_FILE, before === null ? "created" : before === guide ? "unchanged" : "updated");
85
+ }
86
+
87
+ record("AGENTS.md", mergeSection(path.join(cwd, "AGENTS.md"), BRIEF));
88
+
89
+ // Claude Code reads CLAUDE.md, which can import AGENTS.md rather than repeat it.
90
+ const claude = path.join(cwd, "CLAUDE.md");
91
+ const claudeText = fs.existsSync(claude) ? fs.readFileSync(claude, "utf8") : null;
92
+ if (claudeText === null) {
93
+ fs.writeFileSync(claude, "@AGENTS.md\n");
94
+ record("CLAUDE.md", "created");
95
+ } else if (!/^@AGENTS\.md\s*$/m.test(claudeText)) {
96
+ record("CLAUDE.md", mergeSection(claude, "@AGENTS.md"));
97
+ } else {
98
+ record("CLAUDE.md", "unchanged");
99
+ }
100
+
101
+ record(".mcp.json", mergeJson(path.join(cwd, ".mcp.json"), (data) => ({ ...data, mcpServers: { ...(data.mcpServers ?? {}), mlola: SERVER } })));
102
+
103
+ if (fs.existsSync(path.join(cwd, ".cursor"))) {
104
+ record(".cursor/mcp.json", mergeJson(path.join(cwd, ".cursor", "mcp.json"), (data) => ({ ...data, mcpServers: { ...(data.mcpServers ?? {}), mlola: SERVER } })));
105
+ const rule = path.join(cwd, ".cursor", "rules", "mlola.mdc");
106
+ const text = `---\ndescription: Mlola UI, how interface code is built and styled in this project\nalwaysApply: true\n---\n\n${BRIEF.replace(/^## .*\n\n/, "")}\n`;
107
+ const before = fs.existsSync(rule) ? fs.readFileSync(rule, "utf8") : null;
108
+ if (before !== text) {
109
+ fs.mkdirSync(path.dirname(rule), { recursive: true });
110
+ fs.writeFileSync(rule, text);
111
+ }
112
+ record(".cursor/rules/mlola.mdc", before === null ? "created" : before === text ? "unchanged" : "updated");
113
+ }
114
+
115
+ if (fs.existsSync(path.join(cwd, ".vscode"))) {
116
+ record(".vscode/mcp.json", mergeJson(path.join(cwd, ".vscode", "mcp.json"), (data) => ({ ...data, servers: { ...(data.servers ?? {}), mlola: { type: "stdio", ...SERVER } } })));
117
+ }
118
+ return results;
119
+ }
package/src/cli.js CHANGED
@@ -7,17 +7,21 @@ import {
7
7
  targetRoot,
8
8
  writeDefaultConfig,
9
9
  } from "./config.js";
10
+ import { writeAgentFiles } from "./agents.js";
10
11
  import { addAssets, assetHint, loadAssetIndex } from "./assets.js";
11
12
  import { runDoctor } from "./doctor.js";
12
13
  import { collectEngineDependencies, installItems } from "./installer.js";
13
14
  import { loadCatalogNames, loadRegistry, resolveItems } from "./registry.js";
14
15
  import { clearCredentials, fetchProItems, GUIDE_FILENAME, hostFrom, readCredentials, saveCredentials, verifyToken, writeProGuide, writeProStyles } from "./pro.js";
16
+ import { serveMcp } from "./mcp.js";
15
17
  import { pullTheme } from "./theme-pull.js";
16
18
 
17
19
  const HELP = `Mlola UI — source-copy components with Theme
18
20
 
19
21
  Usage:
20
- mlola-ui init [--force]
22
+ mlola-ui init [--force] [--no-agents]
23
+ mlola-ui agents
24
+ mlola-ui mcp
21
25
  mlola-ui add <name...> [--overwrite] [--install|--yes]
22
26
  mlola-ui add asset <id...> [--overwrite]
23
27
  mlola-ui login <token> [--host <url>]
@@ -27,15 +31,25 @@ Usage:
27
31
  mlola-ui theme pull <theme-id | url> [--host <url>] [--overwrite]
28
32
 
29
33
  Examples:
30
- npx mlola-ui init
34
+ npx mlola-ui init (also tells your coding agents about Mlola)
31
35
  npx mlola-ui add button card
32
36
  npx mlola-ui login mlp_… (Mlola Pro: a token from /account)
33
37
  npx mlola-ui add conversation bot
34
38
  npx mlola-ui add asset empty-inbox orb
35
39
  npx mlola-ui list --kind block
36
40
  npx mlola-ui theme pull https://ui.mlola.com/t/th-4k2x9qf7wz3m
41
+ npx mlola-ui mcp (an MCP server for Claude Code, Cursor, Codex…)
37
42
  `;
38
43
 
44
+ const CLI_VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
45
+
46
+ function reportAgents(results, output) {
47
+ const changed = results.filter((entry) => entry.status !== "unchanged");
48
+ for (const { file, status } of changed) output.log(`✓ ${status[0].toUpperCase()}${status.slice(1)} ${file}`);
49
+ if (!changed.length) output.log("• Agent instructions are up to date");
50
+ output.log("Coding agents now know this UI is Mlola: AGENTS.md points them at mlola.agents.md, and .mcp.json starts the Mlola MCP server.");
51
+ }
52
+
39
53
  function hasFlag(args, ...names) {
40
54
  return args.some((argument) => names.includes(argument));
41
55
  }
@@ -113,6 +127,17 @@ export async function run(argv, options = {}) {
113
127
  output.log(
114
128
  `Install the engines you use, starting with: npm install ${config.engine.engine ?? "@mlola-ui/engine"} ${config.engine.motion}`,
115
129
  );
130
+ if (!hasFlag(args, "--no-agents")) reportAgents(writeAgentFiles(cwd), output);
131
+ return 0;
132
+ }
133
+
134
+ if (command === "agents") {
135
+ reportAgents(writeAgentFiles(cwd), output);
136
+ return 0;
137
+ }
138
+
139
+ if (command === "mcp") {
140
+ await serveMcp({ cwd, run, version: CLI_VERSION });
116
141
  return 0;
117
142
  }
118
143
 
@@ -167,7 +192,7 @@ export async function run(argv, options = {}) {
167
192
  if (!token) throw new Error(`Usage: mlola-ui login <token>. Create a token at ${host}/account.`);
168
193
  const { licence } = await verifyToken(host, token, fetcher);
169
194
  const filename = saveCredentials(env, token, host === hostFrom(env) ? undefined : host);
170
- output.log(`✓ Logged in to Mlola Pro (${licence.plan} licence). Token saved to ${filename}.`);
195
+ output.log(`✓ Logged in to Mlola Pro (${licence.plan} license). Token saved to ${filename}.`);
171
196
  return 0;
172
197
  }
173
198
 
@@ -212,7 +237,7 @@ export async function run(argv, options = {}) {
212
237
  if (!credentials) {
213
238
  throw new Error(
214
239
  `${proNames.map((name) => `"${name}"`).join(", ")} ${proNames.length === 1 ? "is" : "are"} part of Mlola Pro. ` +
215
- `With a licence, create a token at ${host}/account and run "npx mlola-ui login <token>". See ${host}/pricing to get Pro.`,
240
+ `With a license, create a token at ${host}/account and run "npx mlola-ui login <token>". See ${host}/pricing to get Pro.`,
216
241
  );
217
242
  }
218
243
  const delivered = await fetchProItems(host, credentials.token, proNames, fetcher);
@@ -0,0 +1,235 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_CONFIG, interpolateTarget } from "./config.js";
4
+ import { BUNDLED_REGISTRY_ROOT, loadRegistry, readJson, sourcePath } from "./registry.js";
5
+
6
+ /**
7
+ * What the CLI knows about Mlola UI, as answers rather than files: every
8
+ * component and catalog item, the tokens, the rules for new UI, and the
9
+ * element contract markup is checked against. All of it is read from the
10
+ * registry bundled with this CLI, so an answer always matches the version
11
+ * that `add` installs, and it works offline.
12
+ */
13
+
14
+ const cache = new Map();
15
+ function bundled(name) {
16
+ if (!cache.has(name)) {
17
+ const filename = path.join(BUNDLED_REGISTRY_ROOT, name);
18
+ cache.set(name, fs.existsSync(filename) ? (name.endsWith(".json") ? readJson(filename) : fs.readFileSync(filename, "utf8")) : null);
19
+ }
20
+ return cache.get(name);
21
+ }
22
+
23
+ const KIND = { "registry:ui": "component", "registry:block": "block", "registry:page": "page", "registry:template": "template" };
24
+
25
+ /** Every item, free and Pro, in one shape. */
26
+ export function allItems() {
27
+ const registry = loadRegistry();
28
+ const free = registry.items.map((item) => ({
29
+ name: item.name,
30
+ kind: KIND[item.type] ?? "component",
31
+ tier: "free",
32
+ title: item.title,
33
+ description: item.description,
34
+ category: item.category,
35
+ tags: item.tags ?? [],
36
+ variants: item.variants ?? [],
37
+ sizes: item.sizes ?? [],
38
+ }));
39
+ const pro = (bundled("catalog.json")?.items ?? []).map((item) => ({
40
+ name: item.name,
41
+ kind: KIND[item.type] ?? "component",
42
+ tier: "pro",
43
+ title: item.title,
44
+ description: item.description,
45
+ category: item.category,
46
+ tags: item.tags ?? [],
47
+ variants: [],
48
+ sizes: [],
49
+ }));
50
+ return [...free, ...pro];
51
+ }
52
+
53
+ /** Items matching a query: every word must appear in the name, title, description, category or tags. */
54
+ export function searchItems({ query = "", kind, category, tier } = {}) {
55
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
56
+ return allItems()
57
+ .filter((item) => (!kind || item.kind === kind) && (!category || item.category === category) && (!tier || item.tier === tier))
58
+ .map((item) => {
59
+ const haystack = [item.name, item.title, item.description, item.category, ...item.tags].join(" ").toLowerCase();
60
+ const score = words.reduce((sum, word) => sum + (item.name === word ? 5 : item.name.includes(word) ? 3 : haystack.includes(word) ? 1 : -100), 0);
61
+ return { item, score };
62
+ })
63
+ .filter(({ score }) => score >= 0)
64
+ .sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name))
65
+ .map(({ item }) => item);
66
+ }
67
+
68
+ /** The classes a component styles (its own prefix) and the attributes each reacts to. */
69
+ function elementsOf(name) {
70
+ const contract = bundled("contract.json");
71
+ if (!contract) return [];
72
+ const prefix = `ml-${name}`;
73
+ return contract.classes
74
+ .filter((cls) => cls === prefix || cls.startsWith(`${prefix}-`))
75
+ .map((cls) => ({ class: cls, attributes: contract.elements[cls] ?? {} }));
76
+ }
77
+
78
+ function projectConfig(cwd) {
79
+ try {
80
+ return readJson(path.join(cwd, "mlola.config.json"));
81
+ } catch {
82
+ return DEFAULT_CONFIG;
83
+ }
84
+ }
85
+
86
+ /** Everything an agent needs to use one item correctly. */
87
+ export function describeItem(name, { cwd = process.cwd(), includeSource = false } = {}) {
88
+ const registry = loadRegistry();
89
+ const item = registry.items.find((entry) => entry.name === name);
90
+ if (!item) {
91
+ const pro = (bundled("catalog.json")?.items ?? []).find((entry) => entry.name === name);
92
+ if (pro) {
93
+ return {
94
+ name,
95
+ tier: "pro",
96
+ kind: KIND[pro.type] ?? "component",
97
+ title: pro.title,
98
+ description: pro.description,
99
+ category: pro.category,
100
+ install: `npx mlola-ui login <token> # once, with a token from https://ui.mlola.com/account\nnpx mlola-ui add ${name}`,
101
+ note: "Part of Mlola Pro. Its source arrives with a license token; its classes and attributes are listed in mlola-pro.agents.md after the first Pro install.",
102
+ };
103
+ }
104
+ const close = allItems().filter((entry) => entry.name.includes(name) || name.includes(entry.name)).map((entry) => entry.name).slice(0, 5);
105
+ throw new Error(`No item named "${name}".${close.length ? ` Did you mean: ${close.join(", ")}?` : ""} Search with search_components.`);
106
+ }
107
+ const config = projectConfig(cwd);
108
+ const usage = item.usage
109
+ ? { import: `import { ${item.usage.exportName} } from "${interpolateTarget(item.usage.importPath, config, "import")}";`, exportName: item.usage.exportName }
110
+ : null;
111
+ const result = {
112
+ name,
113
+ tier: "free",
114
+ kind: KIND[item.type] ?? "component",
115
+ title: item.title,
116
+ description: item.description,
117
+ category: item.category,
118
+ variants: item.variants ?? [],
119
+ sizes: item.sizes ?? [],
120
+ usage,
121
+ install: `npx mlola-ui add ${name}`,
122
+ dependsOn: item.registryDependencies ?? [],
123
+ engineDependencies: item.engineDependencies ?? {},
124
+ elements: elementsOf(name),
125
+ };
126
+ if (includeSource) {
127
+ result.source = (item.files ?? [])
128
+ .filter((file) => !file.path.includes("/_internal/"))
129
+ .map((file) => ({ path: interpolateTarget(file.target, config), content: fs.readFileSync(sourcePath(file.path), "utf8") }));
130
+ }
131
+ return result;
132
+ }
133
+
134
+ export function designData() {
135
+ return bundled("agents.json");
136
+ }
137
+
138
+ export function designGuide() {
139
+ return bundled("agents.md");
140
+ }
141
+
142
+ export function tokens(group) {
143
+ const data = designData();
144
+ if (!data) return [];
145
+ const groups = data.tokens;
146
+ if (!group) return groups;
147
+ const wanted = group.toLowerCase();
148
+ return groups.filter((entry) => entry.group.toLowerCase().includes(wanted) || entry.names.some((name) => name.includes(wanted)));
149
+ }
150
+
151
+ export function themes() {
152
+ const registry = loadRegistry();
153
+ return registry.themes ?? designData()?.themes ?? [];
154
+ }
155
+
156
+ /* ── Checking markup against the contract ─────────────────────────── */
157
+
158
+ const SHARED = new Set(["data-tone", "data-variant", "data-size", "data-state", "data-status"]);
159
+ const UTILITY = /^(-?(m|p)[trblxy]?-\d|flex$|grid$|block$|inline|hidden$|items-|justify-|gap-|w-|h-|min-|max-|text-(xs|sm|base|lg|xl|\d|[a-z]+-\d)|font-(bold|medium|semibold|light)|bg-|border-|rounded|shadow|ring-|space-[xy]-|leading-|tracking-|opacity-|z-\d|absolute$|relative$|fixed$|sticky$|overflow-|col-span|row-span|sm:|md:|lg:|xl:|hover:|focus:|dark:)/;
160
+ const COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i;
161
+
162
+ /**
163
+ * Checks HTML or JSX against the element contract. It reads string-valued
164
+ * class, className, style and data-* attributes; expressions in braces are
165
+ * skipped, since their value is only known at runtime.
166
+ */
167
+ export function checkMarkup(markup) {
168
+ const contract = bundled("contract.json");
169
+ const known = new Set(contract?.classes ?? []);
170
+ const proPrefixes = (bundled("catalog.json")?.items ?? []).map((item) => `ml-${item.name}`);
171
+ const themeIds = new Set(themes().map((theme) => theme.id));
172
+ const issues = [];
173
+ const note = (severity, tag, message, fix) => issues.push({ severity, element: tag.slice(0, 120), message, ...(fix ? { fix } : {}) });
174
+
175
+ for (const match of markup.matchAll(/<([A-Za-z][\w.-]*)((?:\s+(?:[^\s"'>={}]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}))?))*)\s*\/?>/g)) {
176
+ const tag = match[0];
177
+ const attributes = new Map();
178
+ for (const attribute of match[2].matchAll(/([^\s"'>={}]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\{(?:[^{}]|\{[^{}]*\})*\})))?/g)) {
179
+ const [, name, double, single, expression] = attribute;
180
+ const value = double ?? single ?? (expression ? /^\{\s*["'`]([^"'`$]*)["'`]\s*\}$/.exec(expression)?.[1] ?? null : "");
181
+ attributes.set(name, value);
182
+ if (expression) attributes.set(`${name}:raw`, expression);
183
+ }
184
+ const classValue = attributes.get("class") ?? attributes.get("className");
185
+ const classes = typeof classValue === "string" ? classValue.split(/\s+/).filter(Boolean) : [];
186
+ const mlola = classes.filter((cls) => cls.startsWith("ml-"));
187
+
188
+ for (const cls of mlola) {
189
+ if (known.has(cls) || proPrefixes.some((prefix) => cls === prefix || cls.startsWith(`${prefix}-`))) continue;
190
+ const base = [...known].find((candidate) => cls.startsWith(`${candidate}-`) && elementsOf(candidate.slice(3)).length);
191
+ note(
192
+ "error",
193
+ tag,
194
+ `"${cls}" is not a Mlola class.`,
195
+ base ? `Mlola has no variant classes: use "${base}" with a data-* attribute (data-variant, data-tone, data-size).` : "Search with search_components, or name your own element with your own prefix (not ml-).",
196
+ );
197
+ }
198
+ const utilities = classes.filter((cls) => !cls.startsWith("ml-") && UTILITY.test(cls));
199
+ if (utilities.length) {
200
+ note("warning", tag, `Utility classes (${utilities.slice(0, 4).join(" ")}) do nothing here: Mlola ships no utility framework.`, "Use a component or composition primitive; for your own CSS, read --ml-* tokens.");
201
+ }
202
+
203
+ for (const [name, value] of attributes) {
204
+ if (!name.startsWith("data-") || typeof value !== "string") continue;
205
+ if (name === "data-theme" && value && !themeIds.has(value) && !/^th-[0-9a-z]{12}$/.test(value)) {
206
+ note("error", tag, `data-theme="${value}" is not a theme.`, `Use one of: ${[...themeIds].join(", ")}, or a Studio theme id.`);
207
+ }
208
+ if (name === "data-mode" && !["light", "dark"].includes(value)) {
209
+ note("error", tag, `data-mode is "light" or "dark"; "${value}" belongs in an attribute of your own.`, "data-theme and data-mode belong to the engine. Use data-kind or data-state for a component's own meaning.");
210
+ }
211
+ if (!SHARED.has(name) || !mlola.length) continue;
212
+ const allowed = new Set();
213
+ let reacts = false;
214
+ for (const cls of mlola) {
215
+ const values = contract?.elements?.[cls]?.[name];
216
+ if (values) {
217
+ reacts = true;
218
+ for (const entry of values) allowed.add(entry);
219
+ }
220
+ }
221
+ if (reacts && allowed.size && !allowed.has(value)) {
222
+ note("error", tag, `${name}="${value}" is not a value ${mlola.join(" ")} reacts to.`, `Allowed: ${[...allowed].sort().join(", ")}.`);
223
+ } else if (!reacts && mlola.every((cls) => known.has(cls))) {
224
+ note("warning", tag, `${name} has no effect on ${mlola.join(" ")}.`, `See get_component for the attributes it reacts to.`);
225
+ }
226
+ }
227
+
228
+ // A style string, or a JSX style object, with a color written into it.
229
+ const style = attributes.get("style") ?? attributes.get("style:raw");
230
+ if (typeof style === "string" && COLOR.test(style)) {
231
+ note("error", tag, "A color is written by hand in style.", "Read a token: var(--ml-text), var(--ml-primary-text), var(--ml-surface)… (get_tokens).");
232
+ }
233
+ }
234
+ return issues;
235
+ }