@rfdtech/components 1.22.0 → 2.0.1
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/README.md +209 -81
- package/dist/adapters/next-adapter.d.ts.map +1 -1
- package/dist/components/app-header/AppHeader.d.ts.map +1 -1
- package/dist/components/app-header/AppHeaderNotificationItem.d.ts +3 -0
- package/dist/components/app-header/AppHeaderNotificationItem.d.ts.map +1 -0
- package/dist/components/app-header/AppHeaderProfile.d.ts +5 -0
- package/dist/components/app-header/AppHeaderProfile.d.ts.map +1 -1
- package/dist/components/app-header/index.d.ts +3 -1
- package/dist/components/app-header/index.d.ts.map +1 -1
- package/dist/components/app-switcher/AppIconTile.d.ts +10 -0
- package/dist/components/app-switcher/AppIconTile.d.ts.map +1 -0
- package/dist/components/app-switcher/AppSwitcher.d.ts +1 -1
- package/dist/components/app-switcher/AppSwitcher.d.ts.map +1 -1
- package/dist/components/app-switcher/AppSwitcherItem.d.ts.map +1 -1
- package/dist/components/app-switcher/IconPatterns.d.ts +8 -0
- package/dist/components/app-switcher/IconPatterns.d.ts.map +1 -0
- package/dist/components/app-switcher/SystemAppIcon.d.ts.map +1 -1
- package/dist/components/app-switcher/index.d.ts +2 -0
- package/dist/components/app-switcher/index.d.ts.map +1 -1
- package/dist/components/avatar/Avatar.d.ts.map +1 -1
- package/dist/components/bulk-import-modal/BulkImportModal.d.ts.map +1 -1
- package/dist/components/bulk-import-modal/steps/ValidateDataStep.d.ts.map +1 -1
- package/dist/components/button/Button.d.ts +1 -1
- package/dist/components/button/Button.d.ts.map +1 -1
- package/dist/components/button/Spinner.d.ts.map +1 -1
- package/dist/components/combobox/Combobox.d.ts +4 -0
- package/dist/components/combobox/Combobox.d.ts.map +1 -0
- package/dist/components/combobox/Combobox.test.d.ts +2 -0
- package/dist/components/combobox/Combobox.test.d.ts.map +1 -0
- package/dist/components/combobox/index.d.ts +3 -0
- package/dist/components/combobox/index.d.ts.map +1 -0
- package/dist/components/country-selector/CountrySelector.d.ts +7 -0
- package/dist/components/country-selector/CountrySelector.d.ts.map +1 -1
- package/dist/components/country-selector/index.d.ts +1 -0
- package/dist/components/country-selector/index.d.ts.map +1 -1
- package/dist/components/date-range-selector/DateRangeSelector.d.ts +2 -2
- package/dist/components/date-range-selector/DateRangeSelector.d.ts.map +1 -1
- package/dist/components/date-range-selector/index.d.ts +1 -1
- package/dist/components/date-range-selector/index.d.ts.map +1 -1
- package/dist/components/date-selector/DateSelector.d.ts.map +1 -1
- package/dist/components/dropdown/Dropdown.d.ts +1 -1
- package/dist/components/dropdown/Dropdown.d.ts.map +1 -1
- package/dist/components/export-button/ExportButton.d.ts +3 -0
- package/dist/components/export-button/ExportButton.d.ts.map +1 -0
- package/dist/components/export-button/ExportButton.test.d.ts +2 -0
- package/dist/components/export-button/ExportButton.test.d.ts.map +1 -0
- package/dist/components/export-button/index.d.ts +3 -0
- package/dist/components/export-button/index.d.ts.map +1 -0
- package/dist/components/launchpad/Launchpad.d.ts +4 -0
- package/dist/components/launchpad/Launchpad.d.ts.map +1 -0
- package/dist/components/launchpad/Launchpad.test.d.ts +2 -0
- package/dist/components/launchpad/Launchpad.test.d.ts.map +1 -0
- package/dist/components/launchpad/LaunchpadGridIcon.d.ts +2 -0
- package/dist/components/launchpad/LaunchpadGridIcon.d.ts.map +1 -0
- package/dist/components/launchpad/LaunchpadIconTile.d.ts +10 -0
- package/dist/components/launchpad/LaunchpadIconTile.d.ts.map +1 -0
- package/dist/components/launchpad/LaunchpadItem.d.ts +8 -0
- package/dist/components/launchpad/LaunchpadItem.d.ts.map +1 -0
- package/dist/components/launchpad/LaunchpadOverlays.d.ts +3 -0
- package/dist/components/launchpad/LaunchpadOverlays.d.ts.map +1 -0
- package/dist/components/launchpad/SystemLaunchpadIcon.d.ts +5 -0
- package/dist/components/launchpad/SystemLaunchpadIcon.d.ts.map +1 -0
- package/dist/components/launchpad/hooks/useLaunchpad.d.ts +3 -0
- package/dist/components/launchpad/hooks/useLaunchpad.d.ts.map +1 -0
- package/dist/components/launchpad/index.d.ts +10 -0
- package/dist/components/launchpad/index.d.ts.map +1 -0
- package/dist/components/metric-card/MetricCard.d.ts.map +1 -1
- package/dist/components/network-operator/NetworkOperator.d.ts +4 -0
- package/dist/components/network-operator/NetworkOperator.d.ts.map +1 -1
- package/dist/components/network-operator/index.d.ts +1 -0
- package/dist/components/network-operator/index.d.ts.map +1 -1
- package/dist/components/notice/Notice.d.ts +4 -0
- package/dist/components/notice/Notice.d.ts.map +1 -0
- package/dist/components/notice/Notice.test.d.ts +2 -0
- package/dist/components/notice/Notice.test.d.ts.map +1 -0
- package/dist/components/notice/index.d.ts +3 -0
- package/dist/components/notice/index.d.ts.map +1 -0
- package/dist/components/phone-number-input/PhoneNumberInput.d.ts.map +1 -1
- package/dist/components/popup/Popup.d.ts +15 -0
- package/dist/components/popup/Popup.d.ts.map +1 -0
- package/dist/components/popup/Popup.test.d.ts +2 -0
- package/dist/components/popup/Popup.test.d.ts.map +1 -0
- package/dist/components/popup/index.d.ts +3 -0
- package/dist/components/popup/index.d.ts.map +1 -0
- package/dist/components/profile-popover/ProfilePopover.d.ts +4 -0
- package/dist/components/profile-popover/ProfilePopover.d.ts.map +1 -0
- package/dist/components/profile-popover/ProfilePopover.test.d.ts +2 -0
- package/dist/components/profile-popover/ProfilePopover.test.d.ts.map +1 -0
- package/dist/components/profile-popover/index.d.ts +3 -0
- package/dist/components/profile-popover/index.d.ts.map +1 -0
- package/dist/components/role-select/RoleSelect.d.ts +4 -0
- package/dist/components/role-select/RoleSelect.d.ts.map +1 -0
- package/dist/components/role-select/RoleSelect.test.d.ts +2 -0
- package/dist/components/role-select/RoleSelect.test.d.ts.map +1 -0
- package/dist/components/role-select/index.d.ts +3 -0
- package/dist/components/role-select/index.d.ts.map +1 -0
- package/dist/components/section-header/SectionHeader.d.ts +7 -0
- package/dist/components/section-header/SectionHeader.d.ts.map +1 -0
- package/dist/components/section-header/SectionHeader.test.d.ts +2 -0
- package/dist/components/section-header/SectionHeader.test.d.ts.map +1 -0
- package/dist/components/section-header/index.d.ts +3 -0
- package/dist/components/section-header/index.d.ts.map +1 -0
- package/dist/components/sidebar/Sidebar.d.ts +1 -1
- package/dist/components/sidebar/Sidebar.d.ts.map +1 -1
- package/dist/components/sidebar/SidebarContext.d.ts +6 -0
- package/dist/components/sidebar/SidebarContext.d.ts.map +1 -1
- package/dist/components/sidebar/index.d.ts +1 -1
- package/dist/components/sidebar/index.d.ts.map +1 -1
- package/dist/components/switch/Switch.d.ts +4 -0
- package/dist/components/switch/Switch.d.ts.map +1 -0
- package/dist/components/switch/Switch.test.d.ts +2 -0
- package/dist/components/switch/Switch.test.d.ts.map +1 -0
- package/dist/components/switch/index.d.ts +3 -0
- package/dist/components/switch/index.d.ts.map +1 -0
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/TableHeader.d.ts.map +1 -1
- package/dist/components/table/TablePagination.d.ts.map +1 -1
- package/dist/components/table/index.d.ts +1 -1
- package/dist/components/table/index.d.ts.map +1 -1
- package/dist/components/theme/ThemeProvider.d.ts.map +1 -1
- package/dist/components/theme/cletTheme.d.ts +11 -0
- package/dist/components/theme/cletTheme.d.ts.map +1 -0
- package/dist/components/theme/cletTheme.test-d.d.ts +2 -0
- package/dist/components/theme/cletTheme.test-d.d.ts.map +1 -0
- package/dist/components/theme/cletTheme.test.d.ts +2 -0
- package/dist/components/theme/cletTheme.test.d.ts.map +1 -0
- package/dist/components/theme/index.d.ts +3 -1
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/upload-field/UploadField.d.ts +5 -0
- package/dist/components/upload-field/UploadField.d.ts.map +1 -1
- package/dist/components/upload-field/index.d.ts +2 -2
- package/dist/components/upload-field/index.d.ts.map +1 -1
- package/dist/generated/components.theme.d.ts +313 -0
- package/dist/generated/components.theme.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useHasMounted.d.ts +10 -0
- package/dist/hooks/useHasMounted.d.ts.map +1 -0
- package/dist/hooks/useTableFilter.d.ts.map +1 -1
- package/dist/hooks/useTablePagination.d.ts.map +1 -1
- package/dist/hooks/useTableState.d.ts.map +1 -1
- package/dist/index.cjs +84 -56
- package/dist/index.css +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19025 -31594
- package/dist/mcp/cli.js +75 -0
- package/dist/mcp/docs.js +122 -0
- package/dist/mcp/generated/components.json +3601 -0
- package/dist/mcp/generated/examples.json +1233 -0
- package/dist/mcp/generated/index.meta.json +252 -0
- package/dist/mcp/generated/rules.json +629 -0
- package/dist/mcp/generated/search.json +1626 -0
- package/dist/mcp/generated/tokens.json +1022 -0
- package/dist/mcp/index.js +337 -0
- package/dist/mcp/indexer.js +370 -0
- package/dist/mcp/installers/claude.js +37 -0
- package/dist/mcp/installers/codex.js +24 -0
- package/dist/mcp/installers/cursor.js +33 -0
- package/dist/mcp/installers/doctor.js +36 -0
- package/dist/mcp/installers/opencode.js +36 -0
- package/dist/mcp/installers/setup.js +34 -0
- package/dist/mcp/installers/util.js +41 -0
- package/dist/mcp/paths.js +63 -0
- package/dist/mcp/skill/SKILL.md +36 -0
- package/dist/mcp/skill/image-to-components.md +49 -0
- package/dist/next-index.d.ts.map +1 -1
- package/dist/next.cjs +104 -94
- package/dist/next.css +1 -1
- package/dist/next.js +16275 -29977
- package/dist/types/app-header.d.ts +52 -13
- package/dist/types/app-header.d.ts.map +1 -1
- package/dist/types/app-switcher.d.ts +10 -1
- package/dist/types/app-switcher.d.ts.map +1 -1
- package/dist/types/avatar.d.ts +9 -0
- package/dist/types/avatar.d.ts.map +1 -1
- package/dist/types/combobox.d.ts +43 -0
- package/dist/types/combobox.d.ts.map +1 -0
- package/dist/types/country-selector.d.ts +4 -0
- package/dist/types/country-selector.d.ts.map +1 -1
- package/dist/types/date-range-selector.d.ts +11 -0
- package/dist/types/date-range-selector.d.ts.map +1 -1
- package/dist/types/dropdown.d.ts +8 -0
- package/dist/types/dropdown.d.ts.map +1 -1
- package/dist/types/export-button.d.ts +24 -0
- package/dist/types/export-button.d.ts.map +1 -0
- package/dist/types/launchpad.d.ts +79 -0
- package/dist/types/launchpad.d.ts.map +1 -0
- package/dist/types/metric-card.d.ts +4 -0
- package/dist/types/metric-card.d.ts.map +1 -1
- package/dist/types/network-operator.d.ts +8 -0
- package/dist/types/network-operator.d.ts.map +1 -1
- package/dist/types/notice.d.ts +24 -0
- package/dist/types/notice.d.ts.map +1 -0
- package/dist/types/popup.d.ts +43 -0
- package/dist/types/popup.d.ts.map +1 -0
- package/dist/types/profile-popover.d.ts +97 -0
- package/dist/types/profile-popover.d.ts.map +1 -0
- package/dist/types/role-select.d.ts +28 -0
- package/dist/types/role-select.d.ts.map +1 -0
- package/dist/types/section-header.d.ts +26 -0
- package/dist/types/section-header.d.ts.map +1 -0
- package/dist/types/sidebar.d.ts +5 -0
- package/dist/types/sidebar.d.ts.map +1 -1
- package/dist/types/switch.d.ts +24 -0
- package/dist/types/switch.d.ts.map +1 -0
- package/dist/types/table.d.ts +23 -1
- package/dist/types/table.d.ts.map +1 -1
- package/dist/types/theme.d.ts +28 -8
- package/dist/types/theme.d.ts.map +1 -1
- package/dist/types/upload-field.d.ts +23 -0
- package/dist/types/upload-field.d.ts.map +1 -1
- package/dist/utils/export.d.ts +15 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/stringToColor.d.ts +25 -0
- package/dist/utils/stringToColor.d.ts.map +1 -1
- package/package.json +29 -10
- package/scripts/postinstall.mjs +40 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { GENERATED_DIR, SOURCE, hasRepoSource } from "./paths.js";
|
|
5
|
+
function quoted(key, block) {
|
|
6
|
+
return block.match(new RegExp(`${key}:\\s*["']([^"']*)["']`))?.[1];
|
|
7
|
+
}
|
|
8
|
+
function quotedArray(key, block) {
|
|
9
|
+
const m = block.match(new RegExp(`${key}:\\s*\\[([\\s\\S]*?)\\]`));
|
|
10
|
+
if (!m)
|
|
11
|
+
return [];
|
|
12
|
+
return [...m[1].matchAll(/["']([^"']*)["']/g)].map((x) => x[1]);
|
|
13
|
+
}
|
|
14
|
+
function parseMeta(source) {
|
|
15
|
+
const block = source.match(/export const meta\s*=\s*{([\s\S]*?)};/)?.[1] ?? "";
|
|
16
|
+
return {
|
|
17
|
+
title: quoted("title", block) ?? "",
|
|
18
|
+
description: quoted("description", block),
|
|
19
|
+
category: quoted("category", block),
|
|
20
|
+
keywords: quotedArray("keywords", block),
|
|
21
|
+
since: quoted("since", block),
|
|
22
|
+
status: quoted("status", block),
|
|
23
|
+
related: quotedArray("related", block),
|
|
24
|
+
import: quoted("import", block),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Everything from the first top-level heading onward — drops the meta block and import statements. */
|
|
28
|
+
function stripMetaAndImports(source) {
|
|
29
|
+
const heading = source.match(/^#\s+.+$/m);
|
|
30
|
+
if (!heading || heading.index === undefined)
|
|
31
|
+
return source.trim();
|
|
32
|
+
return source.slice(heading.index).trim();
|
|
33
|
+
}
|
|
34
|
+
function extractHeadings(body) {
|
|
35
|
+
return [...body.matchAll(/^#{1,3}\s+(.+)$/gm)].map((m) => m[1].trim());
|
|
36
|
+
}
|
|
37
|
+
// ───────────────────────── examples ─────────────────────────
|
|
38
|
+
const RAW_IMPORT_RE = /import\s+(\w+)\s+from\s+["']\.\.\/previews\/examples\/([^"']+)\?raw["'];?/g;
|
|
39
|
+
function extractRawImports(source) {
|
|
40
|
+
return [...source.matchAll(RAW_IMPORT_RE)].map((m) => ({
|
|
41
|
+
importName: m[1],
|
|
42
|
+
file: m[2],
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
function extractUsedComponents(source) {
|
|
46
|
+
const used = new Set();
|
|
47
|
+
const re = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']@rfdtech\/components["'];?/g;
|
|
48
|
+
for (const m of source.matchAll(re)) {
|
|
49
|
+
for (const raw of m[1].split(",")) {
|
|
50
|
+
const name = raw.trim().split(/\s+as\s+/)[0].trim();
|
|
51
|
+
if (name)
|
|
52
|
+
used.add(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...used].sort();
|
|
56
|
+
}
|
|
57
|
+
function titleFromExampleFile(file) {
|
|
58
|
+
const base = file.replace(/\.example\.tsx$/, "");
|
|
59
|
+
return base
|
|
60
|
+
.split("-")
|
|
61
|
+
.map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
|
|
62
|
+
.join(" ");
|
|
63
|
+
}
|
|
64
|
+
async function buildRules() {
|
|
65
|
+
const rulesByCategory = {};
|
|
66
|
+
const componentToRuleIds = {};
|
|
67
|
+
if (!existsSync(SOURCE.rules))
|
|
68
|
+
return { rulesByCategory, componentToRuleIds };
|
|
69
|
+
const files = (await readdir(SOURCE.rules)).filter((f) => f.endsWith(".md")).sort();
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const category = file.replace(/\.md$/, "");
|
|
72
|
+
const raw = await readFile(path.join(SOURCE.rules, file), "utf8");
|
|
73
|
+
const blocks = raw.split("===RULE===").slice(1); // [0] is the "# Title" preamble
|
|
74
|
+
const rules = [];
|
|
75
|
+
for (const rawBlock of blocks) {
|
|
76
|
+
const block = rawBlock.replace(/^\n/, "");
|
|
77
|
+
const headerEnd = block.indexOf("\n\n");
|
|
78
|
+
const headerPart = headerEnd === -1 ? block : block.slice(0, headerEnd);
|
|
79
|
+
const body = headerEnd === -1 ? "" : block.slice(headerEnd + 2).trim();
|
|
80
|
+
const fields = {};
|
|
81
|
+
for (const line of headerPart.split("\n")) {
|
|
82
|
+
const idx = line.indexOf(":");
|
|
83
|
+
if (idx === -1)
|
|
84
|
+
continue;
|
|
85
|
+
fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
86
|
+
}
|
|
87
|
+
if (!fields.id)
|
|
88
|
+
continue;
|
|
89
|
+
const components = (fields.components ?? "")
|
|
90
|
+
.split(",")
|
|
91
|
+
.map((s) => s.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
const rule = {
|
|
94
|
+
id: fields.id,
|
|
95
|
+
title: fields.title ?? "",
|
|
96
|
+
severity: fields.severity ?? "info",
|
|
97
|
+
components,
|
|
98
|
+
body,
|
|
99
|
+
};
|
|
100
|
+
rules.push(rule);
|
|
101
|
+
for (const c of components) {
|
|
102
|
+
(componentToRuleIds[c] ??= []).push(rule.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
rulesByCategory[category] = rules;
|
|
106
|
+
}
|
|
107
|
+
return { rulesByCategory, componentToRuleIds };
|
|
108
|
+
}
|
|
109
|
+
// ───────────────────────── tokens ─────────────────────────
|
|
110
|
+
// Mirrors scripts/generate-theme-tokens.mjs's `classify()` so tokens.json's
|
|
111
|
+
// "group" field lines up with the same value-type buckets cletTheme() enforces
|
|
112
|
+
// at the type level (color/length/shadow/zIndex/opacity/duration/misc).
|
|
113
|
+
function classifyToken(cssVar) {
|
|
114
|
+
const local = cssVar.replace(/^--clet-/, "");
|
|
115
|
+
if (/radius/.test(local))
|
|
116
|
+
return "length";
|
|
117
|
+
if (/shadow/.test(local))
|
|
118
|
+
return "shadow";
|
|
119
|
+
if (/^z(-|$)/.test(local))
|
|
120
|
+
return "zIndex";
|
|
121
|
+
if (/font/.test(local))
|
|
122
|
+
return "font";
|
|
123
|
+
if (/opacity/.test(local))
|
|
124
|
+
return "opacity";
|
|
125
|
+
if (/(size|gap|width|height|padding|margin|px$|py$|-px-|-py-)/.test(local))
|
|
126
|
+
return "length";
|
|
127
|
+
if (/(duration|transition|delay|stagger)/.test(local))
|
|
128
|
+
return "duration";
|
|
129
|
+
if (/(color|^bg$|-bg$|bg-|background|border|accent|track|success|warning|error|primary|secondary|surface|hover|overlay|text|focus|current|highlight|shimmer)/.test(local))
|
|
130
|
+
return "color";
|
|
131
|
+
return "misc";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parses src/generated/components.theme.ts — the file cletTheme() itself is
|
|
135
|
+
* typed against — for the authoritative list of every overridable token.
|
|
136
|
+
* This is what makes tokens.json/get_tokens() match the real runtime API
|
|
137
|
+
* surface (including ~70+ component-scoped tokens like --clet-card-padding
|
|
138
|
+
* that never appear in the 3 global theme CSS files).
|
|
139
|
+
*/
|
|
140
|
+
async function readTokenRegistry() {
|
|
141
|
+
if (!existsSync(SOURCE.generatedTheme))
|
|
142
|
+
return [];
|
|
143
|
+
const source = await readFile(SOURCE.generatedTheme, "utf8");
|
|
144
|
+
const entries = [];
|
|
145
|
+
const globalBlock = source.match(/export const CLET_GLOBAL_TOKEN_VARS[^{]*\{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
146
|
+
for (const m of globalBlock.matchAll(/^\s{2}\w+:\s*"(--clet-[\w-]+)",\s*$/gm)) {
|
|
147
|
+
entries.push({ cssVar: m[1] });
|
|
148
|
+
}
|
|
149
|
+
const componentSection = source.match(/export const CLET_COMPONENT_TOKEN_VARS[\s\S]*?=\s*\{([\s\S]*)\n\};\s*$/)?.[1] ?? "";
|
|
150
|
+
for (const compMatch of componentSection.matchAll(/^\s{2}(\w+):\s*\{([\s\S]*?)\n\s{2}\},\s*$/gm)) {
|
|
151
|
+
const [, component, body] = compMatch;
|
|
152
|
+
for (const m of body.matchAll(/^\s{4}\w+:\s*"(--clet-[\w-]+)",\s*$/gm)) {
|
|
153
|
+
entries.push({ component, cssVar: m[1] });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
/** "AppHeader" -> "app-header" — inverse of generate-theme-tokens.mjs's toPascalCase(). */
|
|
159
|
+
function pascalToKebab(pascal) {
|
|
160
|
+
return pascal.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
/** Component-scoped values, keyed by bare local name (no "--clet-" prefix). */
|
|
163
|
+
async function readComponentCssValues(slug) {
|
|
164
|
+
const values = new Map();
|
|
165
|
+
const stylesDir = path.join(SOURCE.componentsDir, slug, "styles");
|
|
166
|
+
if (!existsSync(stylesDir))
|
|
167
|
+
return values;
|
|
168
|
+
for (const file of await readdir(stylesDir)) {
|
|
169
|
+
if (!file.endsWith(".css"))
|
|
170
|
+
continue;
|
|
171
|
+
const css = await readFile(path.join(stylesDir, file), "utf8");
|
|
172
|
+
for (const m of css.matchAll(/--clet-([\w-]+)\s*:\s*([^;]+);/g)) {
|
|
173
|
+
if (!values.has(m[1]))
|
|
174
|
+
values.set(m[1], m[2].trim()); // first declaration = base value
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return values;
|
|
178
|
+
}
|
|
179
|
+
async function buildTokens() {
|
|
180
|
+
const tokens = {};
|
|
181
|
+
// Global token VALUES, keyed by bare local name, per theme.
|
|
182
|
+
const globalValues = {};
|
|
183
|
+
const files = { base: "base.css", light: "light.css", dark: "dark.css" };
|
|
184
|
+
for (const [theme, file] of Object.entries(files)) {
|
|
185
|
+
const p = path.join(SOURCE.themeDir, file);
|
|
186
|
+
if (!existsSync(p))
|
|
187
|
+
continue;
|
|
188
|
+
const css = await readFile(p, "utf8");
|
|
189
|
+
for (const m of css.matchAll(/--clet-([\w-]+)\s*:\s*([^;]+);/g)) {
|
|
190
|
+
(globalValues[m[1]] ??= {})[theme] = m[2].trim();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const registry = await readTokenRegistry();
|
|
194
|
+
const byComponent = new Map();
|
|
195
|
+
for (const entry of registry) {
|
|
196
|
+
if (!entry.component) {
|
|
197
|
+
const localName = entry.cssVar.replace(/^--clet-/, "");
|
|
198
|
+
tokens[entry.cssVar] = {
|
|
199
|
+
group: classifyToken(entry.cssVar),
|
|
200
|
+
scope: "global",
|
|
201
|
+
...globalValues[localName],
|
|
202
|
+
};
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const list = byComponent.get(entry.component) ?? [];
|
|
206
|
+
list.push(entry.cssVar);
|
|
207
|
+
byComponent.set(entry.component, list);
|
|
208
|
+
}
|
|
209
|
+
// Component-scoped VALUES: each component's own styles/*.css (fallback
|
|
210
|
+
// registry when a token isn't declared where expected — best-effort, first
|
|
211
|
+
// declaration wins; a component may set the same token again in a media
|
|
212
|
+
// query for a responsive override, which this doesn't capture separately).
|
|
213
|
+
for (const [component, cssVars] of byComponent) {
|
|
214
|
+
const slug = pascalToKebab(component);
|
|
215
|
+
const cssValues = await readComponentCssValues(slug);
|
|
216
|
+
for (const cssVar of cssVars) {
|
|
217
|
+
const localName = cssVar.replace(/^--clet-/, "");
|
|
218
|
+
const base = cssValues.get(localName);
|
|
219
|
+
tokens[cssVar] = {
|
|
220
|
+
group: classifyToken(cssVar),
|
|
221
|
+
scope: "component",
|
|
222
|
+
component: slug,
|
|
223
|
+
...(base ? { base } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return tokens;
|
|
228
|
+
}
|
|
229
|
+
export async function buildIndex(outDir = GENERATED_DIR) {
|
|
230
|
+
if (!hasRepoSource) {
|
|
231
|
+
throw new Error("buildIndex() requires live repo source (demo/docs, src/types) which isn't present — " +
|
|
232
|
+
"this looks like a published install; it should only ever read the bundled generated/ snapshot.");
|
|
233
|
+
}
|
|
234
|
+
await mkdir(outDir, { recursive: true });
|
|
235
|
+
const pageFiles = (await readdir(SOURCE.pages)).filter((f) => f.endsWith(".mdx")).sort();
|
|
236
|
+
const exampleFiles = (await readdir(SOURCE.examples)).filter((f) => f.endsWith(".example.tsx"));
|
|
237
|
+
const pageSources = new Map();
|
|
238
|
+
for (const file of pageFiles) {
|
|
239
|
+
pageSources.set(file.replace(/\.mdx$/, ""), await readFile(path.join(SOURCE.pages, file), "utf8"));
|
|
240
|
+
}
|
|
241
|
+
const pageImports = new Map();
|
|
242
|
+
const globalImported = new Set();
|
|
243
|
+
for (const [slug, src] of pageSources) {
|
|
244
|
+
const imports = extractRawImports(src);
|
|
245
|
+
pageImports.set(slug, imports);
|
|
246
|
+
imports.forEach((i) => globalImported.add(i.file));
|
|
247
|
+
}
|
|
248
|
+
const exampleSourceCache = new Map();
|
|
249
|
+
async function getExampleSource(file) {
|
|
250
|
+
if (!exampleSourceCache.has(file)) {
|
|
251
|
+
try {
|
|
252
|
+
exampleSourceCache.set(file, await readFile(path.join(SOURCE.examples, file), "utf8"));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
exampleSourceCache.set(file, `// Could not read ${file}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return exampleSourceCache.get(file);
|
|
259
|
+
}
|
|
260
|
+
const { rulesByCategory, componentToRuleIds } = await buildRules();
|
|
261
|
+
const tokens = await buildTokens();
|
|
262
|
+
const components = {};
|
|
263
|
+
const examplesOut = {};
|
|
264
|
+
const searchEntries = [];
|
|
265
|
+
let exampleCount = 0;
|
|
266
|
+
for (const [slug, src] of pageSources) {
|
|
267
|
+
const meta = parseMeta(src);
|
|
268
|
+
const docsBody = stripMetaAndImports(src);
|
|
269
|
+
const headings = extractHeadings(docsBody);
|
|
270
|
+
// Hybrid example ownership: explicit `?raw` imports are authoritative;
|
|
271
|
+
// a `<slug>*` glob only fills in files no page has claimed (avoids the
|
|
272
|
+
// form/form-field inversion and similar cross-page naming collisions).
|
|
273
|
+
const importsForSlug = pageImports.get(slug) ?? [];
|
|
274
|
+
const importNameByFile = new Map(importsForSlug.map((i) => [i.file, i.importName]));
|
|
275
|
+
const owned = new Set(importsForSlug.map((i) => i.file));
|
|
276
|
+
for (const file of exampleFiles) {
|
|
277
|
+
if (file.startsWith(slug) && !globalImported.has(file))
|
|
278
|
+
owned.add(file);
|
|
279
|
+
}
|
|
280
|
+
const exampleList = await Promise.all([...owned].sort().map(async (file) => {
|
|
281
|
+
const source = await getExampleSource(file);
|
|
282
|
+
return {
|
|
283
|
+
file,
|
|
284
|
+
importName: importNameByFile.get(file),
|
|
285
|
+
title: titleFromExampleFile(file),
|
|
286
|
+
components: extractUsedComponents(source),
|
|
287
|
+
source,
|
|
288
|
+
};
|
|
289
|
+
}));
|
|
290
|
+
examplesOut[slug] = exampleList;
|
|
291
|
+
exampleCount += exampleList.length;
|
|
292
|
+
const typesPath = path.join(SOURCE.types, `${slug}.ts`);
|
|
293
|
+
const types = existsSync(typesPath) ? await readFile(typesPath, "utf8") : null;
|
|
294
|
+
const ruleIds = componentToRuleIds[slug] ?? [];
|
|
295
|
+
components[slug] = {
|
|
296
|
+
meta: { slug, ...meta },
|
|
297
|
+
docs: docsBody,
|
|
298
|
+
headings,
|
|
299
|
+
types,
|
|
300
|
+
examples: exampleList,
|
|
301
|
+
ruleIds,
|
|
302
|
+
};
|
|
303
|
+
searchEntries.push({
|
|
304
|
+
slug,
|
|
305
|
+
name: meta.title || slug,
|
|
306
|
+
category: meta.category,
|
|
307
|
+
keywords: meta.keywords,
|
|
308
|
+
description: meta.description,
|
|
309
|
+
headings,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
await writeFile(path.join(outDir, "components.json"), JSON.stringify(components, null, 2));
|
|
313
|
+
await writeFile(path.join(outDir, "examples.json"), JSON.stringify(examplesOut, null, 2));
|
|
314
|
+
await writeFile(path.join(outDir, "search.json"), JSON.stringify(searchEntries, null, 2));
|
|
315
|
+
await writeFile(path.join(outDir, "rules.json"), JSON.stringify(rulesByCategory, null, 2));
|
|
316
|
+
await writeFile(path.join(outDir, "tokens.json"), JSON.stringify(tokens, null, 2));
|
|
317
|
+
const builtFrom = {};
|
|
318
|
+
for (const dir of [SOURCE.pages, SOURCE.examples, SOURCE.types, SOURCE.rules, SOURCE.themeDir]) {
|
|
319
|
+
if (!existsSync(dir))
|
|
320
|
+
continue;
|
|
321
|
+
for (const file of await readdir(dir)) {
|
|
322
|
+
const filePath = path.join(dir, file);
|
|
323
|
+
const info = await stat(filePath);
|
|
324
|
+
if (info.isFile())
|
|
325
|
+
builtFrom[filePath] = info.mtimeMs;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// The generated token registry drives buildTokens() identity/grouping —
|
|
329
|
+
// track it so editing component CSS + regenerating it (or forgetting to)
|
|
330
|
+
// is visible to the staleness check, not just the 3 global theme files.
|
|
331
|
+
if (existsSync(SOURCE.generatedTheme)) {
|
|
332
|
+
builtFrom[SOURCE.generatedTheme] = (await stat(SOURCE.generatedTheme)).mtimeMs;
|
|
333
|
+
}
|
|
334
|
+
// Component-scoped token VALUES come from each component's own styles/*.css
|
|
335
|
+
// — track those too, so a value-only CSS edit (no registry change) still
|
|
336
|
+
// triggers a rebuild.
|
|
337
|
+
if (existsSync(SOURCE.componentsDir)) {
|
|
338
|
+
for (const entry of await readdir(SOURCE.componentsDir)) {
|
|
339
|
+
const stylesDir = path.join(SOURCE.componentsDir, entry, "styles");
|
|
340
|
+
if (!existsSync(stylesDir))
|
|
341
|
+
continue;
|
|
342
|
+
for (const file of await readdir(stylesDir)) {
|
|
343
|
+
if (!file.endsWith(".css"))
|
|
344
|
+
continue;
|
|
345
|
+
const filePath = path.join(stylesDir, file);
|
|
346
|
+
builtFrom[filePath] = (await stat(filePath)).mtimeMs;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const ruleCount = Object.values(rulesByCategory).reduce((n, rs) => n + rs.length, 0);
|
|
351
|
+
const result = {
|
|
352
|
+
componentCount: Object.keys(components).length,
|
|
353
|
+
exampleCount,
|
|
354
|
+
ruleCount,
|
|
355
|
+
tokenCount: Object.keys(tokens).length,
|
|
356
|
+
};
|
|
357
|
+
await writeFile(path.join(outDir, "index.meta.json"), JSON.stringify({ builtAt: Date.now(), builtFrom, ...result }, null, 2));
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
// Runnable directly: `tsx src/indexer.ts` (dev) or `node dist/indexer.js` (build).
|
|
361
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
362
|
+
buildIndex()
|
|
363
|
+
.then((result) => {
|
|
364
|
+
console.log(`Index built: ${JSON.stringify(result)}`);
|
|
365
|
+
})
|
|
366
|
+
.catch((err) => {
|
|
367
|
+
console.error(err);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { appendPointerOnce, mergeMcpServers, readJsonIfExists, writeJson, } from "./util.js";
|
|
5
|
+
export function detect(cwd) {
|
|
6
|
+
return existsSync(path.join(cwd, ".claude")) || existsSync(path.join(cwd, ".mcp.json"));
|
|
7
|
+
}
|
|
8
|
+
async function installSkill(cwd, slug, skillSourcePath) {
|
|
9
|
+
const skillDir = path.join(cwd, ".claude", "skills", slug);
|
|
10
|
+
await mkdir(skillDir, { recursive: true });
|
|
11
|
+
const skillDest = path.join(skillDir, "SKILL.md");
|
|
12
|
+
const skillSource = await readFile(skillSourcePath, "utf8");
|
|
13
|
+
const skillChanged = !existsSync(skillDest) || (await readFile(skillDest, "utf8")) !== skillSource;
|
|
14
|
+
await copyFile(skillSourcePath, skillDest);
|
|
15
|
+
return skillChanged;
|
|
16
|
+
}
|
|
17
|
+
export async function install(cwd, skillSourcePath, skillImageSourcePath) {
|
|
18
|
+
const mcpJsonPath = path.join(cwd, ".mcp.json");
|
|
19
|
+
const existing = await readJsonIfExists(mcpJsonPath);
|
|
20
|
+
const { data, changed: mcpChanged } = mergeMcpServers(existing);
|
|
21
|
+
await writeJson(mcpJsonPath, data);
|
|
22
|
+
const skillChanged = await installSkill(cwd, "rfdtech-ui", skillSourcePath);
|
|
23
|
+
const imageSkillChanged = await installSkill(cwd, "image-to-components", skillImageSourcePath);
|
|
24
|
+
const claudeMdChanged = await appendPointerOnce(path.join(cwd, "CLAUDE.md"), "rfdtech-ui", "<!-- rfdtech-ui -->\nBefore writing UI code, see `.claude/skills/rfdtech-ui/SKILL.md` for " +
|
|
25
|
+
"`@rfdtech/components` conventions (search components, use authoritative types, follow the rules). " +
|
|
26
|
+
"Given a screenshot, image, mockup, or description of a screen to build, see " +
|
|
27
|
+
"`.claude/skills/image-to-components/SKILL.md` first.\n", "Project notes");
|
|
28
|
+
const changed = mcpChanged || skillChanged || imageSkillChanged || claudeMdChanged;
|
|
29
|
+
return {
|
|
30
|
+
name: "Claude Code",
|
|
31
|
+
changed,
|
|
32
|
+
detail: `.mcp.json${mcpChanged ? " (updated)" : " (already set)"}, ` +
|
|
33
|
+
`.claude/skills/rfdtech-ui/SKILL.md${skillChanged ? " (updated)" : " (already set)"}, ` +
|
|
34
|
+
`.claude/skills/image-to-components/SKILL.md${imageSkillChanged ? " (updated)" : " (already set)"}, ` +
|
|
35
|
+
`CLAUDE.md${claudeMdChanged ? " (updated)" : " (already set)"}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const CODEX_DIR = path.join(os.homedir(), ".codex");
|
|
6
|
+
const CONFIG_PATH = path.join(CODEX_DIR, "config.toml");
|
|
7
|
+
const MARKER = "[mcp_servers.rfdtech-ui]";
|
|
8
|
+
export function detect() {
|
|
9
|
+
return existsSync(CODEX_DIR);
|
|
10
|
+
}
|
|
11
|
+
// Codex config is TOML. Rather than pull in a TOML parser/writer for one
|
|
12
|
+
// block, we do a text-level idempotent append — safe because we only ever
|
|
13
|
+
// check for and add our own uniquely-named `[mcp_servers.rfdtech-ui]` table
|
|
14
|
+
// and never touch any other section.
|
|
15
|
+
export async function install() {
|
|
16
|
+
await mkdir(CODEX_DIR, { recursive: true });
|
|
17
|
+
const existing = existsSync(CONFIG_PATH) ? await readFile(CONFIG_PATH, "utf8") : "";
|
|
18
|
+
if (existing.includes(MARKER)) {
|
|
19
|
+
return { name: "Codex", changed: false, detail: "~/.codex/config.toml (already set)" };
|
|
20
|
+
}
|
|
21
|
+
const block = `\n${MARKER}\ncommand = "npx"\nargs = ["-y", "@rfdtech/components-mcp"]\n`;
|
|
22
|
+
await writeFile(CONFIG_PATH, existing.replace(/\s*$/, "") + "\n" + block, "utf8");
|
|
23
|
+
return { name: "Codex", changed: true, detail: "~/.codex/config.toml (updated)" };
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mergeMcpServers, readJsonIfExists, writeJson } from "./util.js";
|
|
5
|
+
export function detect(cwd) {
|
|
6
|
+
return existsSync(path.join(cwd, ".cursor"));
|
|
7
|
+
}
|
|
8
|
+
async function installRule(cwd, slug, description, skillSourcePath) {
|
|
9
|
+
const rulesDir = path.join(cwd, ".cursor", "rules");
|
|
10
|
+
await mkdir(rulesDir, { recursive: true });
|
|
11
|
+
const rulesPath = path.join(rulesDir, `${slug}.mdc`);
|
|
12
|
+
const skill = await readFile(skillSourcePath, "utf8");
|
|
13
|
+
const mdc = `---\ndescription: ${description}\nglobs: ["**/*.tsx", "**/*.ts"]\nalwaysApply: false\n---\n\n${skill}`;
|
|
14
|
+
const rulesChanged = !existsSync(rulesPath) || (await readFile(rulesPath, "utf8")) !== mdc;
|
|
15
|
+
await writeFile(rulesPath, mdc, "utf8");
|
|
16
|
+
return rulesChanged;
|
|
17
|
+
}
|
|
18
|
+
export async function install(cwd, skillSourcePath, skillImageSourcePath) {
|
|
19
|
+
const mcpJsonPath = path.join(cwd, ".cursor", "mcp.json");
|
|
20
|
+
const existing = await readJsonIfExists(mcpJsonPath);
|
|
21
|
+
const { data, changed: mcpChanged } = mergeMcpServers(existing);
|
|
22
|
+
await writeJson(mcpJsonPath, data);
|
|
23
|
+
const rulesChanged = await installRule(cwd, "rfdtech-ui", "GSL (@rfdtech/components) usage rules", skillSourcePath);
|
|
24
|
+
const imageRulesChanged = await installRule(cwd, "image-to-components", "Translate screenshots/mockups into GSL (@rfdtech/components) components", skillImageSourcePath);
|
|
25
|
+
const changed = mcpChanged || rulesChanged || imageRulesChanged;
|
|
26
|
+
return {
|
|
27
|
+
name: "Cursor",
|
|
28
|
+
changed,
|
|
29
|
+
detail: `.cursor/mcp.json${mcpChanged ? " (updated)" : " (already set)"}, ` +
|
|
30
|
+
`.cursor/rules/rfdtech-ui.mdc${rulesChanged ? " (updated)" : " (already set)"}, ` +
|
|
31
|
+
`.cursor/rules/image-to-components.mdc${imageRulesChanged ? " (updated)" : " (already set)"}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { GENERATED_DIR, hasRepoSource } from "../paths.js";
|
|
5
|
+
import * as claude from "./claude.js";
|
|
6
|
+
import * as cursor from "./cursor.js";
|
|
7
|
+
import * as codex from "./codex.js";
|
|
8
|
+
import * as opencode from "./opencode.js";
|
|
9
|
+
export async function runDoctor(cwd = process.cwd()) {
|
|
10
|
+
console.log("Checking AI setup...\n");
|
|
11
|
+
const metaPath = path.join(GENERATED_DIR, "index.meta.json");
|
|
12
|
+
if (existsSync(metaPath)) {
|
|
13
|
+
const meta = JSON.parse(await readFile(metaPath, "utf8"));
|
|
14
|
+
console.log(`✓ Index found — ${meta.componentCount} components, ${meta.exampleCount} examples, ` +
|
|
15
|
+
`${meta.ruleCount} rules, ${meta.tokenCount} tokens`);
|
|
16
|
+
}
|
|
17
|
+
else if (hasRepoSource) {
|
|
18
|
+
console.log("✗ Index missing — run `rfdui index` (or just start the MCP server, which builds it automatically)");
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log("✗ No bundled index found in this install — the package may be corrupt; try reinstalling");
|
|
22
|
+
}
|
|
23
|
+
const tools = [
|
|
24
|
+
{ name: "Claude Code", ok: claude.detect(cwd) },
|
|
25
|
+
{ name: "Cursor", ok: cursor.detect(cwd) },
|
|
26
|
+
{ name: "Codex", ok: codex.detect() },
|
|
27
|
+
{ name: "OpenCode", ok: opencode.detect(cwd) },
|
|
28
|
+
];
|
|
29
|
+
for (const t of tools) {
|
|
30
|
+
console.log(`${t.ok ? "✓" : "•"} ${t.name}${t.ok ? " detected" : " not detected"}`);
|
|
31
|
+
}
|
|
32
|
+
console.log(hasRepoSource
|
|
33
|
+
? "\n✓ Running inside the gsl-components repo — index rebuilds automatically when source changes."
|
|
34
|
+
: "\n• Running as an installed package — serving the bundled index snapshot (no live rebuild).");
|
|
35
|
+
console.log("\nRun `rfdui setup` to (re)configure detected tools.");
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { appendPointerOnce, readJsonIfExists, writeJson } from "./util.js";
|
|
5
|
+
export function detect(cwd) {
|
|
6
|
+
return (existsSync(path.join(cwd, "opencode.json")) ||
|
|
7
|
+
existsSync(path.join(os.homedir(), ".config", "opencode")));
|
|
8
|
+
}
|
|
9
|
+
// Verified against https://opencode.ai/docs/mcp-servers/ and
|
|
10
|
+
// https://opencode.ai/docs/config/: top-level `mcp` key in a project-root
|
|
11
|
+
// `opencode.json`, each entry `{ type: "local", command: [...], enabled }`.
|
|
12
|
+
export async function install(cwd) {
|
|
13
|
+
const configPath = path.join(cwd, "opencode.json");
|
|
14
|
+
const existing = await readJsonIfExists(configPath);
|
|
15
|
+
const isNewFile = !existing;
|
|
16
|
+
const data = existing && typeof existing === "object" ? { ...existing } : {};
|
|
17
|
+
if (isNewFile)
|
|
18
|
+
data.$schema ??= "https://opencode.ai/config.json";
|
|
19
|
+
const mcp = { ...data.mcp };
|
|
20
|
+
const desired = { type: "local", command: ["npx", "-y", "@rfdtech/components-mcp"], enabled: true };
|
|
21
|
+
const already = JSON.stringify(mcp["rfdtech-ui"]) === JSON.stringify(desired);
|
|
22
|
+
mcp["rfdtech-ui"] = desired;
|
|
23
|
+
data.mcp = mcp;
|
|
24
|
+
await writeJson(configPath, data);
|
|
25
|
+
const agentsChanged = await appendPointerOnce(path.join(cwd, "AGENTS.md"), "rfdtech-ui", "<!-- rfdtech-ui -->\nUse the `rfdtech-ui` MCP server (`search_components`, `get_component`, " +
|
|
26
|
+
"`get_rules`) before building UI with `@rfdtech/components`. Given a screenshot, image, " +
|
|
27
|
+
"mockup, or description of a screen to build, decompose it and map every element to an " +
|
|
28
|
+
"existing `@rfdtech/components` component/variant via the MCP before writing markup — " +
|
|
29
|
+
"only build custom UI if the user explicitly asks for it.\n", "Agent notes");
|
|
30
|
+
const changed = !already || agentsChanged;
|
|
31
|
+
return {
|
|
32
|
+
name: "OpenCode",
|
|
33
|
+
changed,
|
|
34
|
+
detail: `opencode.json${already ? " (already set)" : " (updated)"}, AGENTS.md${agentsChanged ? " (updated)" : " (already set)"}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, copyFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SKILL_SOURCE_PATH, SKILL_IMAGE_SOURCE_PATH } from "../paths.js";
|
|
4
|
+
import * as claude from "./claude.js";
|
|
5
|
+
import * as cursor from "./cursor.js";
|
|
6
|
+
import * as codex from "./codex.js";
|
|
7
|
+
import * as opencode from "./opencode.js";
|
|
8
|
+
export async function runSetup(cwd = process.cwd()) {
|
|
9
|
+
console.log("rfdui setup — detecting AI tools...\n");
|
|
10
|
+
// Always drop the cross-tool skills, regardless of which specific tool (if any) is detected.
|
|
11
|
+
const aiDir = path.join(cwd, ".ai");
|
|
12
|
+
await mkdir(aiDir, { recursive: true });
|
|
13
|
+
await copyFile(SKILL_SOURCE_PATH, path.join(aiDir, "SKILL.md"));
|
|
14
|
+
await copyFile(SKILL_IMAGE_SOURCE_PATH, path.join(aiDir, "image-to-components.md"));
|
|
15
|
+
console.log("✓ .ai/SKILL.md, .ai/image-to-components.md written\n");
|
|
16
|
+
const results = [];
|
|
17
|
+
if (claude.detect(cwd))
|
|
18
|
+
results.push(await claude.install(cwd, SKILL_SOURCE_PATH, SKILL_IMAGE_SOURCE_PATH));
|
|
19
|
+
if (cursor.detect(cwd))
|
|
20
|
+
results.push(await cursor.install(cwd, SKILL_SOURCE_PATH, SKILL_IMAGE_SOURCE_PATH));
|
|
21
|
+
if (codex.detect())
|
|
22
|
+
results.push(await codex.install());
|
|
23
|
+
if (opencode.detect(cwd))
|
|
24
|
+
results.push(await opencode.install(cwd));
|
|
25
|
+
if (results.length === 0) {
|
|
26
|
+
console.log("No supported AI tool detected (looked for .claude/, .mcp.json, .cursor/, ~/.codex/, " +
|
|
27
|
+
"opencode.json, ~/.config/opencode).\n" +
|
|
28
|
+
"The MCP server is still available directly: `npx @rfdtech/components-mcp` or `rfdui mcp`.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const r of results) {
|
|
32
|
+
console.log(`${r.changed ? "✓" : "•"} ${r.name}: ${r.detail}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export async function readJsonIfExists(filePath) {
|
|
5
|
+
if (!existsSync(filePath))
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function writeJson(filePath, data) {
|
|
15
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
17
|
+
}
|
|
18
|
+
/** The MCP server identity every installer wires up, consistently named across tools. */
|
|
19
|
+
export const MCP_SERVER_NAME = "rfdtech-ui";
|
|
20
|
+
export const MCP_SERVER_COMMAND = { command: "npx", args: ["-y", "@rfdtech/components-mcp"] };
|
|
21
|
+
/** Merge `{ mcpServers: { "rfdtech-ui": {...} } }` into an existing config object without touching anything else. */
|
|
22
|
+
export function mergeMcpServers(existing) {
|
|
23
|
+
const data = existing && typeof existing === "object" ? { ...existing } : {};
|
|
24
|
+
const servers = { ...data.mcpServers };
|
|
25
|
+
const already = JSON.stringify(servers[MCP_SERVER_NAME]) === JSON.stringify(MCP_SERVER_COMMAND);
|
|
26
|
+
servers[MCP_SERVER_NAME] = MCP_SERVER_COMMAND;
|
|
27
|
+
data.mcpServers = servers;
|
|
28
|
+
return { data, changed: !already };
|
|
29
|
+
}
|
|
30
|
+
/** Append `marker`-tagged content to a markdown file once; no-op if already present. */
|
|
31
|
+
export async function appendPointerOnce(filePath, marker, block, fallbackHeading) {
|
|
32
|
+
if (existsSync(filePath)) {
|
|
33
|
+
const content = await readFile(filePath, "utf8");
|
|
34
|
+
if (content.includes(marker))
|
|
35
|
+
return false;
|
|
36
|
+
await writeFile(filePath, content.replace(/\s*$/, "") + "\n\n" + block, "utf8");
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
await writeFile(filePath, `# ${fallbackHeading}\n\n${block}`, "utf8");
|
|
40
|
+
return true;
|
|
41
|
+
}
|