@collectui/bdui 0.1.2

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/bin/bdui.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // BoardUI CLI 入口 — 极简 commander 复刻(零依赖)
3
+ import { run } from "../src/cli.js";
4
+
5
+ run(process.argv.slice(2)).catch((error) => {
6
+ console.error(`✗ ${error.message}`);
7
+ process.exitCode = 1;
8
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@collectui/bdui",
3
+ "version": "0.1.2",
4
+ "description": "Add BoardUI components to your project.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "bdui": "bin/bdui.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "dependencies": {
18
+ "commander": "^12.1.0",
19
+ "ora": "^8.1.1",
20
+ "picocolors": "^1.1.1",
21
+ "prompts": "^2.4.2",
22
+ "zod": "^4.5.1"
23
+ }
24
+ }
package/src/add.js ADDED
@@ -0,0 +1,122 @@
1
+ // `bdui add` 复刻 — 递归解析 registryDependencies,落盘文件,装依赖
2
+
3
+ import { spawn } from "child_process";
4
+ import ora from "ora";
5
+ import c from "./color.js";
6
+ import { detectProject, installCommand } from "./project.js";
7
+ import { fetchIndex, fetchItem } from "./registry.js";
8
+ import { writeItemFiles } from "./write.js";
9
+
10
+ export async function installPackages(pm, packages, cwd) {
11
+ if (!packages.length) return;
12
+ const [command, args] = installCommand(pm, packages);
13
+ await new Promise((resolve, reject) => {
14
+ const child = spawn(command, args, {
15
+ cwd,
16
+ stdio: "ignore",
17
+ shell: process.platform === "win32",
18
+ });
19
+ child.on("error", reject);
20
+ child.on("close", (code) =>
21
+ code === 0
22
+ ? resolve()
23
+ : reject(
24
+ new Error(`${command} ${args.join(" ")} exited with code ${code}`)
25
+ )
26
+ );
27
+ });
28
+ }
29
+
30
+ export async function cmdAdd(names, flags) {
31
+ const project = detectProject(flags.dir ?? process.cwd());
32
+ if (!project.pkg)
33
+ throw new Error(
34
+ "No package.json found — run this inside your app project."
35
+ );
36
+
37
+ const index = await fetchIndex(true);
38
+ const wanted = flags.all ? index.items.map((i) => i.name) : names;
39
+
40
+ if (!wanted.length) {
41
+ console.log(
42
+ "No components selected. Run `bdui list` to see what's available."
43
+ );
44
+ return;
45
+ }
46
+
47
+ // 递归收集:name → item(公共 + pro),依赖闭包先装;pro 用 --key
48
+ const key = flags.key;
49
+ const resolved = new Map();
50
+ const missing = [];
51
+ async function collect(name) {
52
+ if (resolved.has(name)) return;
53
+ const meta = index.items.find((i) => i.name === name);
54
+ if (!meta) {
55
+ missing.push(name);
56
+ return;
57
+ }
58
+ const item = await fetchItem(name, key);
59
+ resolved.set(name, item);
60
+ for (const dep of item.registryDependencies ?? []) {
61
+ if (!resolved.has(dep)) await collect(dep);
62
+ }
63
+ }
64
+ for (const name of wanted) await collect(name);
65
+
66
+ if (missing.length) {
67
+ throw new Error(
68
+ `Unknown component(s): ${missing.join(", ")} — run \`bdui list\`.`
69
+ );
70
+ }
71
+
72
+ const outRoot = flags.path ?? project.sourceRoot;
73
+ const deps = new Map(); // pkgName → 带 版本的 spec(去重,优先带版本形式)
74
+ let wrote = 0;
75
+ writeItemFiles.lastWroteStyles = false;
76
+
77
+ for (const item of resolved.values()) {
78
+ for (const dep of item.dependencies ?? []) {
79
+ const pkgName = dep.split("@")[0] || dep;
80
+ if (!project.deps[pkgName]) {
81
+ const prev = deps.get(pkgName);
82
+ if (!prev || (prev === pkgName && dep !== pkgName))
83
+ deps.set(pkgName, dep);
84
+ }
85
+ }
86
+ wrote += await writeItemFiles(item, outRoot, flags);
87
+ }
88
+
89
+ if (deps.size) {
90
+ const specs = [...deps.values()];
91
+ if (flags.noDeps) {
92
+ console.log(
93
+ ` ${c.yellow("deps")} still needed (installDependencies was false): ${project.packageManager} add ${specs.join(" ")}`
94
+ );
95
+ } else {
96
+ console.log(` ${c.cyan("deps")} installing ${specs.join(", ")}`);
97
+ const spin = process.stderr.isTTY
98
+ ? ora({
99
+ text: "Installing dependencies…",
100
+ stream: process.stderr,
101
+ color: "cyan",
102
+ }).start()
103
+ : null;
104
+ try {
105
+ await installPackages(project.packageManager, specs, project.cwd);
106
+ spin?.succeed("Dependencies installed");
107
+ } catch (e) {
108
+ spin?.fail("Dependency installation failed");
109
+ throw e;
110
+ }
111
+ }
112
+ }
113
+ console.log(c.green(`\n✓ ${wrote} file(s) written.`));
114
+ // 与 boardui 同款 Note:样式入口提示(本次写过 styles/ 文件时)
115
+ if (writeItemFiles.lastWroteStyles) {
116
+ console.log(
117
+ c.dim(
118
+ `\nNote: import the BoardUI styles in your root layout or global css:\n import "@/styles/globals.css";`
119
+ )
120
+ );
121
+ }
122
+ }
package/src/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ // 命令解析 — commander(与 boardui 同款),五命令 + 自动 help
2
+ import { Command } from "commander";
3
+ import { readFileSync } from "fs";
4
+ import { cmdAdd } from "./add.js";
5
+ import { cmdInit } from "./init.js";
6
+ import { cmdList } from "./list.js";
7
+ import { cmdMcp } from "./mcp.js";
8
+ import { cmdSkill } from "./skill.js";
9
+
10
+ const VERSION = JSON.parse(
11
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
12
+ ).version;
13
+
14
+ export async function run(argv) {
15
+ const program = new Command();
16
+
17
+ program
18
+ .name("bdui")
19
+ .description("Add BoardUI components to your project.")
20
+ .version(VERSION, "-v, --version", "output the version number")
21
+ .helpOption("-h, --help", "display help for command")
22
+ .helpCommand("help [command]", "display help for command");
23
+
24
+ program
25
+ .command("init")
26
+ .description(
27
+ "Set up BoardUI in an existing React project (deps, theme, utils)."
28
+ )
29
+ .option("-y, --yes", "skip confirmation prompts")
30
+ .option("-o, --overwrite", "overwrite existing files")
31
+ .option(
32
+ "-d, --dir <dir>",
33
+ "target directory (defaults to the project source root)"
34
+ )
35
+ .option(
36
+ "--pro",
37
+ "also install the deps BoardUI Pro components use (recharts, motion, ...)"
38
+ )
39
+ .action(cmdInit);
40
+
41
+ program
42
+ .command("add [components...]")
43
+ .description("Add components to your project.")
44
+ .option("-a, --all", "add every component in the registry")
45
+ .option(
46
+ "-k, --key <key>",
47
+ "license key for Pro components (single command, nothing is saved)"
48
+ )
49
+ .option(
50
+ "-p, --path <path>",
51
+ "custom directory to write component files into"
52
+ )
53
+ .option(
54
+ "-d, --dir <dir>",
55
+ "target directory (defaults to the project source root)"
56
+ )
57
+ .option("-o, --overwrite", "overwrite existing files")
58
+ .option("-y, --yes", "skip prompts (skips existing files, installs deps)")
59
+ .action(cmdAdd);
60
+
61
+ program
62
+ .command("list")
63
+ .description("List all available components.")
64
+ .action(cmdList);
65
+
66
+ program
67
+ .command("mcp")
68
+ .description(
69
+ "Start the BoardUI MCP server (stdio) so AI agents can browse and install components. Pass --key to unlock Pro items for the whole session (embed it in the args of your MCP config)."
70
+ )
71
+ .option(
72
+ "-k, --key <key>",
73
+ "license key for Pro components, active for this server session (nothing is saved)"
74
+ )
75
+ .action((options) => cmdMcp(options));
76
+
77
+ program
78
+ .command("skill")
79
+ .description(
80
+ "Install the BoardUI agent skill (SKILL.md + references) so agents know the design system."
81
+ )
82
+ .option(
83
+ "-g, --global",
84
+ "install to ~/.claude/skills/boardui instead of the project's .claude/skills"
85
+ )
86
+ .option(
87
+ "-c, --client <client>",
88
+ "install to ~/.agents/skills/boardui for clients that read it (codex, gemini)"
89
+ )
90
+ .option("-p, --path <dir>", "custom directory to install the skill into")
91
+ .option("-f, --force", "overwrite an existing install (how you update)")
92
+ .action(cmdSkill);
93
+
94
+ await program.parseAsync(argv, { from: "user" });
95
+ }
package/src/color.js ADDED
@@ -0,0 +1,11 @@
1
+ // picocolors 薄封装:保持各模块在用的具名色 API
2
+ import pc from "picocolors";
3
+
4
+ export default {
5
+ green: pc.green,
6
+ yellow: pc.yellow,
7
+ red: pc.red,
8
+ cyan: pc.cyan,
9
+ dim: pc.dim,
10
+ bold: pc.bold,
11
+ };
package/src/init.js ADDED
@@ -0,0 +1,79 @@
1
+ // `bdui init` — 对齐 boardui init:装 foundations(cx/theme/typography/globals)
2
+ // + agent 规则(AGENTS.md + .cursor/rules),内容全部来自 registry(与官方同源)
3
+
4
+ import ora from "ora";
5
+ import { installPackages } from "./add.js";
6
+ import c from "./color.js";
7
+ import { detectProject } from "./project.js";
8
+ import { fetchItem } from "./registry.js";
9
+ import { applyAgentRules } from "./rules.js";
10
+ import { writeItemFiles } from "./write.js";
11
+
12
+ const FOUNDATIONS = ["cx", "theme", "typography", "globals"];
13
+ const PRO_EXTRA_DEPS = [
14
+ "motion",
15
+ "recharts",
16
+ "react-aria-components",
17
+ "@remixicon/react",
18
+ "@internationalized/date",
19
+ ];
20
+
21
+ export async function cmdInit(flags) {
22
+ const cwd = flags.dir ?? process.cwd();
23
+ const project = detectProject(cwd);
24
+ if (!project.pkg)
25
+ throw new Error("No package.json found — run inside your app project.");
26
+
27
+ console.log(
28
+ `${c.cyan("init")} detected ${project.packageManager}, src=${project.usesSrcDir}, alias=${project.alias}`
29
+ );
30
+
31
+ // npm 依赖:foundation 用到的 + 可选 pro 依赖(官方 --pro 同款集)
32
+ const deps = new Set(["clsx", "tailwind-merge"]);
33
+ if (flags.pro) {
34
+ for (const d of PRO_EXTRA_DEPS) deps.add(d);
35
+ }
36
+
37
+ // foundations 条目落盘(boardui init 同款:theme tokens、type scale、globals、cx)
38
+ const outRoot = flags.path ?? project.sourceRoot;
39
+ let wrote = 0;
40
+ for (const name of FOUNDATIONS) {
41
+ const item = await fetchItem(name);
42
+ for (const dep of item.dependencies ?? []) {
43
+ const pkgName = dep.split("@")[0] || dep;
44
+ if (!project.deps[pkgName]) deps.add(dep);
45
+ }
46
+ wrote += await writeItemFiles(item, outRoot, flags);
47
+ }
48
+
49
+ // agent 规则(AGENTS.md + .cursor/rules/boardui.mdc)
50
+ const ruleLines = await applyAgentRules(cwd);
51
+
52
+ if (deps.size) {
53
+ console.log(` ${c.cyan("deps")} ${[...deps].join(", ")}`);
54
+ const spin = process.stderr.isTTY
55
+ ? ora({
56
+ text: "Installing dependencies…",
57
+ stream: process.stderr,
58
+ color: "cyan",
59
+ }).start()
60
+ : null;
61
+ try {
62
+ await installPackages(project.packageManager, [...deps], cwd);
63
+ spin?.succeed("Dependencies installed");
64
+ } catch (e) {
65
+ spin?.fail("Dependency installation failed");
66
+ throw e;
67
+ }
68
+ }
69
+ console.log(c.green(`\n✓ ${wrote} foundation file(s) written.`));
70
+ for (const line of ruleLines) console.log(line);
71
+ console.log(
72
+ ` ${c.dim('Note: import the styles in your root layout or global css: import "@/styles/globals.css";')}`
73
+ );
74
+ console.log(
75
+ c.green(
76
+ "BoardUI ready. Add components with npx @collectui/bdui add <name>."
77
+ )
78
+ );
79
+ }
package/src/list.js ADDED
@@ -0,0 +1,28 @@
1
+ // `bdui list` 复刻 — 按分组打印 name + description
2
+ import c from "./color.js";
3
+ import { fetchIndex } from "./registry.js";
4
+
5
+ export async function cmdList() {
6
+ const { items } = await fetchIndex(true);
7
+ const groups = new Map();
8
+ for (const item of items) {
9
+ const key = item.bdui2Type ?? "base";
10
+ if (!groups.has(key)) groups.set(key, []);
11
+ groups.get(key).push(item);
12
+ }
13
+ const labels = {
14
+ base: "Base components",
15
+ application: "Application blocks",
16
+ foundations: "Foundations",
17
+ };
18
+ for (const [key, group] of groups) {
19
+ console.log(c.bold(labels[key] ?? key));
20
+ const width = Math.max(...group.map((i) => i.name.length)) + 2;
21
+ for (const item of group) {
22
+ const name = item.name.padEnd(width);
23
+ console.log(` ${name}${item.description ?? ""}`);
24
+ }
25
+ console.log();
26
+ }
27
+ console.log("Add a component with npx @collectui/bdui add <name>");
28
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,408 @@
1
+ // `bdui mcp` — 裸 JSON-RPC 2.0 over stdio 的 MCP server(零依赖)
2
+ // 工具面对齐 boardui MCP(13 个去掉 license 三件套 = 10 个):
3
+ // get_started / list_components / get_component / get_usage_examples /
4
+ // install_components / init_boardui / install_rules / get_skill / install_skill / get_theme
5
+
6
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
7
+ import path from "path";
8
+ import { createInterface } from "readline";
9
+ import { z } from "zod";
10
+ import { cmdAdd } from "./add.js";
11
+ import { detectProject } from "./project.js";
12
+ import { fetchIndex, fetchItem } from "./registry.js";
13
+ import { applyAgentRules } from "./rules.js";
14
+
15
+ const SKILL_ITEM = "skill";
16
+
17
+ const NAME_PATTERN_DOC = "Exact item name from list_components.";
18
+ const DIR_DOC = "Absolute path of the project root (defaults to cwd).";
19
+
20
+ // zod 声明参数 → tools/list 的 inputSchema;tools/call 入口同一 schema 校验
21
+ const SCHEMAS = {
22
+ get_started: z.object({}),
23
+ list_components: z.object({
24
+ tier: z
25
+ .string()
26
+ .optional()
27
+ .describe("'free' or 'pro' to filter; omit for all."),
28
+ }),
29
+ get_component: z.object({
30
+ name: z.string().describe(NAME_PATTERN_DOC),
31
+ key: z
32
+ .string()
33
+ .optional()
34
+ .describe(
35
+ "License key for Pro items; optional when the server was started with --key."
36
+ ),
37
+ }),
38
+ get_usage_examples: z.object({
39
+ name: z.string().describe(NAME_PATTERN_DOC),
40
+ key: z.string().optional(),
41
+ }),
42
+ install_components: z.object({
43
+ names: z.array(z.string()).describe("Items from list_components."),
44
+ key: z
45
+ .string()
46
+ .optional()
47
+ .describe(
48
+ "License key for Pro items (same as `add --key`); optional when the server was started with --key."
49
+ ),
50
+ overwrite: z.boolean().optional(),
51
+ installDependencies: z.boolean().optional(),
52
+ projectDir: z.string().optional().describe(DIR_DOC),
53
+ }),
54
+ init_boardui: z.object({
55
+ pro: z
56
+ .boolean()
57
+ .optional()
58
+ .describe(
59
+ "Also install the deps Pro components use (recharts, motion, ...)."
60
+ ),
61
+ overwrite: z.boolean().optional(),
62
+ projectDir: z.string().optional().describe(DIR_DOC),
63
+ }),
64
+ install_rules: z.object({
65
+ overwrite: z.boolean().optional(),
66
+ projectDir: z.string().optional().describe(DIR_DOC),
67
+ }),
68
+ get_skill: z.object({}),
69
+ install_skill: z.object({
70
+ overwrite: z.boolean().optional(),
71
+ projectDir: z.string().optional().describe(DIR_DOC),
72
+ }),
73
+ get_theme: z.object({}),
74
+ };
75
+
76
+ const DESCRIPTIONS = {
77
+ get_started:
78
+ "Overview of BoardUI and how to use this server: setup, discovering components, installing, and the design rules.",
79
+ list_components:
80
+ "Lists every installable BoardUI item with exact names: free foundations/base/application items and Pro components/templates.",
81
+ get_component:
82
+ "Full detail for one BoardUI item: description, docs URL, npm/BoardUI dependencies, registry dependencies, and every source file with content.",
83
+ get_usage_examples:
84
+ "Working usage snippets for a BoardUI item, straight from its docs page.",
85
+ install_components:
86
+ "Installs BoardUI components/templates into the user's project: writes source files, resolves internal dependencies recursively, and installs npm dependencies.",
87
+ init_boardui:
88
+ "One-time BoardUI setup for an existing React project: installs deps, writes theme tokens, globals, the cx utility, and the always-on agent design rules (AGENTS.md + .cursor/rules).",
89
+ install_rules:
90
+ "Writes BoardUI's always-on design rules into the project: a fenced section in AGENTS.md plus .cursor/rules/boardui.mdc. init_boardui already does this — call this on projects initialized before rules existed, or to pull the latest rules.",
91
+ get_skill:
92
+ "What the BoardUI agent skill is and what it contains: returns the live SKILL.md plus the bundled reference files with their sizes and the install options for every client.",
93
+ install_skill:
94
+ "Writes the BoardUI agent skill into the project's .claude/skills/boardui (SKILL.md + references) so skill-capable agents get always-loaded BoardUI knowledge.",
95
+ get_theme:
96
+ "The BoardUI theme foundations: color primitives + semantic tokens (the source of every color in the system) and how to re-tint the accent ramp.",
97
+ };
98
+
99
+ const TOOLS = Object.entries(SCHEMAS).map(([name, schema]) => ({
100
+ name,
101
+ description: DESCRIPTIONS[name],
102
+ inputSchema: z.toJSONSchema(schema),
103
+ }));
104
+
105
+ function groupLabel(t) {
106
+ return (
107
+ {
108
+ base: "Base components",
109
+ application: "Application blocks",
110
+ foundations: "Foundations",
111
+ }[t] ?? t
112
+ );
113
+ }
114
+
115
+ // serverKey:`bdui mcp --key <key>` 启动内嵌(.mcp.json args),整会话兜底;
116
+ // 调用级 key 参数优先。两者都不落盘,请求结束即消失。
117
+ async function loadItem(name, key, serverKey) {
118
+ return fetchItem(name, key ?? serverKey);
119
+ }
120
+
121
+ async function dispatch(name, args, captured, serverKey) {
122
+ const projectDir = args.projectDir ?? process.cwd();
123
+ switch (name) {
124
+ case "get_started":
125
+ return [
126
+ "BoardUI is a React design system for agentic interfaces: components are copied into your project as plain React + Tailwind files (no runtime package).",
127
+ "",
128
+ "Quick flow:",
129
+ "1. init_boardui — one-time setup (deps, theme tokens, globals, cx, agent rules).",
130
+ "2. list_components — discover; get_component reads source/props.",
131
+ "3. get_usage_examples — working code for an item.",
132
+ "4. install_components — writes files, resolves internal deps recursively, installs npm deps.",
133
+ "",
134
+ "Pro items need the license key. Two ways, both leave nothing stored on this machine:",
135
+ "- Embed it in this server's config once: `npx -y @collectui/bdui mcp --key <key>` (the args block of your MCP config) — every tool call in the session is unlocked.",
136
+ "- Or pass it per call as `key` to install_components (same as the CLI's `add --key <license-key>`).",
137
+ "CLI equivalents: npx @collectui/bdui init | list | add <name> [--key <key>] | skill | mcp [--key <key>].",
138
+ ].join("\n");
139
+
140
+ case "list_components": {
141
+ const idx = await fetchIndex(true);
142
+ const tier = (args.tier ?? "").toLowerCase();
143
+ const isPro = (i) => Boolean(i.pro);
144
+ const want = (i) =>
145
+ tier === "pro" ? isPro(i) : tier === "free" ? !isPro(i) : true;
146
+ const items = idx.items.filter(want);
147
+ const header =
148
+ tier === "pro"
149
+ ? "## Pro components\nPro license required — pass `key` to install_components."
150
+ : tier === "free"
151
+ ? "## Free components"
152
+ : "## All components";
153
+ const lines = [header];
154
+ const groups = new Map();
155
+ for (const i of items) {
156
+ const g = i.bdui2Type ?? "base";
157
+ if (!groups.has(g)) groups.set(g, []);
158
+ groups.get(g).push(i);
159
+ }
160
+ for (const [g, group] of groups) {
161
+ lines.push("", `### ${groupLabel(g)}`);
162
+ for (const i of group)
163
+ lines.push(
164
+ `- ${i.name}${i.pro ? " (pro)" : ""}: ${i.description ?? i.title ?? ""}`
165
+ );
166
+ }
167
+ return lines.join("\n");
168
+ }
169
+
170
+ case "get_component": {
171
+ const item = await loadItem(args.name, args.key, serverKey);
172
+ const meta = item.meta ?? {};
173
+ const lines = [
174
+ `# ${item.title ?? args.name} (${args.name})`,
175
+ item.description ?? "",
176
+ meta.docsUrl ? `Docs: ${meta.docsUrl}` : "",
177
+ "",
178
+ `npm dependencies: ${(item.dependencies ?? []).join(", ") || "none"}`,
179
+ `internal dependencies: ${(item.registryDependencies ?? []).join(", ") || "none"}`,
180
+ `files: ${(item.files ?? []).map((f) => f.path).join(", ")}`,
181
+ ];
182
+ const first = item.files?.[0];
183
+ if (first?.content) {
184
+ lines.push(
185
+ "",
186
+ "## Source (first file)",
187
+ "```tsx",
188
+ first.content,
189
+ "```"
190
+ );
191
+ }
192
+ return lines.filter(Boolean).join("\n");
193
+ }
194
+
195
+ case "get_usage_examples": {
196
+ const item = await loadItem(args.name, args.key, serverKey);
197
+ const meta = item.meta ?? {};
198
+ const lines = [
199
+ `# ${item.title ?? args.name} (${args.name})`,
200
+ item.description ?? "",
201
+ ];
202
+ if (meta.docsUrl) lines.push("", `Docs: ${meta.docsUrl}`);
203
+ if (meta.example)
204
+ lines.push("", "## Example", "```tsx", meta.example, "```");
205
+ if (Array.isArray(meta.snippets) && meta.snippets.length) {
206
+ lines.push("", "## Snippets");
207
+ for (const s of meta.snippets)
208
+ lines.push(
209
+ "",
210
+ "```tsx",
211
+ typeof s === "string" ? s : (s.code ?? JSON.stringify(s)),
212
+ "```"
213
+ );
214
+ }
215
+ if (!(meta.example || meta.snippets?.length))
216
+ lines.push("", "No usage examples recorded for this item.");
217
+ return lines.filter((x) => x !== undefined).join("\n");
218
+ }
219
+
220
+ case "install_components": {
221
+ const flags = {
222
+ overwrite: args.overwrite,
223
+ yes: true,
224
+ key: args.key ?? serverKey,
225
+ ...(args.installDependencies === false ? { noDeps: true } : {}),
226
+ ...(args.projectDir ? { dir: args.projectDir } : {}),
227
+ };
228
+ await cmdAdd(args.names, flags);
229
+ return `installed: ${args.names.join(", ")}\n${captured.join("\n")}`;
230
+ }
231
+
232
+ case "init_boardui": {
233
+ const project = detectProject(projectDir);
234
+ if (!project.pkg) {
235
+ return `No package.json found in ${projectDir}. Pass projectDir with the absolute path of the project root. If the directory is fresh, scaffold an app there first (e.g. npx create-next-app@latest .), then call init_boardui again.`;
236
+ }
237
+ const { cmdInit } = await import("./init.js");
238
+ await cmdInit({
239
+ dir: projectDir,
240
+ pro: args.pro,
241
+ overwrite: args.overwrite,
242
+ yes: true,
243
+ });
244
+ const ruleLines = await applyAgentRules(projectDir);
245
+ return ["Setup complete.", ...captured, ...ruleLines].join("\n");
246
+ }
247
+
248
+ case "install_rules":
249
+ return (await applyAgentRules(projectDir)).join("\n");
250
+
251
+ case "get_skill": {
252
+ const skill = await loadItem(SKILL_ITEM);
253
+ const skillMd = skill.files?.find((f) => f.path.endsWith("SKILL.md"));
254
+ const refs = (skill.files ?? []).filter(
255
+ (f) => !f.path.endsWith("SKILL.md")
256
+ );
257
+ const lines = [
258
+ "The BoardUI agent skill (SKILL.md open standard) gives skill-capable agents always-loaded BoardUI knowledge; the MCP server stays the on-demand companion for live source and installs.",
259
+ "",
260
+ "Files in the skill:",
261
+ `- SKILL.md (${(skillMd?.content ?? "").length.toLocaleString()} bytes)`,
262
+ ...refs.map(
263
+ (f) =>
264
+ `- ${path.posix.basename(f.path)} (${(f.content ?? "").length.toLocaleString()} bytes)`
265
+ ),
266
+ "",
267
+ "Install options: install_skill (this MCP, project .claude/skills/boardui) or the CLI: npx @collectui/bdui skill (project), --global (~/.claude/skills), --client codex (~/.agents/skills, read by Codex and Gemini), --path <dir>, --force (update).",
268
+ ];
269
+ if (skillMd?.content) lines.push("", "---", skillMd.content);
270
+ return lines.join("\n");
271
+ }
272
+
273
+ case "install_skill": {
274
+ const skill = await loadItem(SKILL_ITEM);
275
+ const target = path.join(projectDir, ".claude", "skills", "boardui");
276
+ mkdirSync(target, { recursive: true });
277
+ let n = 0;
278
+ for (const f of skill.files ?? []) {
279
+ // references/ 下的归 references/,其余(SKILL.md)落根
280
+ const rel = f.path.includes("references/")
281
+ ? path.join("references", path.posix.basename(f.path))
282
+ : path.posix.basename(f.path);
283
+ const dest = path.join(target, rel);
284
+ mkdirSync(path.dirname(dest), { recursive: true });
285
+ if (existsSync(dest) && !args.overwrite) continue;
286
+ writeFileSync(dest, f.content ?? "");
287
+ n++;
288
+ }
289
+ return `Skill ${n > 0 ? `installed (${n} files)` : "already present (pass overwrite)"} → ${target}`;
290
+ }
291
+
292
+ case "get_theme": {
293
+ const theme = await loadItem("theme");
294
+ const globals = await loadItem("globals");
295
+ const themeCss = theme.files?.[0]?.content ?? "";
296
+ const lines = [
297
+ "# BoardUI theme foundations",
298
+ "",
299
+ "Every color rides a semantic token (text / background / border / foreground / chart); dark mode flips tokens automatically under `.dark` — never write a dark: override or a raw hex value. The accent ramp drives CTAs and selection states and can be re-tinted to your brand in one override.",
300
+ "",
301
+ "## theme (color primitives + semantic tokens)",
302
+ "```css",
303
+ themeCss.slice(0, 6000),
304
+ "```",
305
+ "",
306
+ "## globals (entry css)",
307
+ "```css",
308
+ (globals.files?.[0]?.content ?? "").slice(0, 3000),
309
+ "```",
310
+ ];
311
+ return lines.join("\n");
312
+ }
313
+
314
+ default:
315
+ throw new Error(`Unknown tool: ${name}`);
316
+ }
317
+ }
318
+
319
+ // CLI 子流程的 console.log 会污染 stdio JSON-RPC 流:调用期间捕获收集
320
+ async function call(name, args, serverKey) {
321
+ const schema = SCHEMAS[name];
322
+ if (!schema) throw new Error(`Unknown tool: ${name}`);
323
+ const parsed = schema.safeParse(args ?? {});
324
+ if (!parsed.success) {
325
+ throw new Error(
326
+ `Invalid arguments for ${name}: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
327
+ );
328
+ }
329
+ const captured = [];
330
+ const originalLog = console.log;
331
+ console.log = (...a) => captured.push(a.join(" "));
332
+ try {
333
+ return await dispatch(name, parsed.data, captured, serverKey);
334
+ } finally {
335
+ console.log = originalLog;
336
+ }
337
+ }
338
+
339
+ export async function cmdMcp(options = {}) {
340
+ const serverKey = options.key ?? undefined; // `bdui mcp --key <key>`:整会话兜底,见 loadItem 注释
341
+ const rl = createInterface({ input: process.stdin });
342
+ const write = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
343
+
344
+ rl.on("line", (line) => {
345
+ if (!line.trim()) return;
346
+ let msg;
347
+ try {
348
+ msg = JSON.parse(line);
349
+ } catch {
350
+ return write({
351
+ jsonrpc: "2.0",
352
+ id: null,
353
+ error: { code: -32_700, message: "Parse error" },
354
+ });
355
+ }
356
+ const { id, method, params } = msg;
357
+ (async () => {
358
+ if (method === "initialize") {
359
+ return write({
360
+ jsonrpc: "2.0",
361
+ id,
362
+ result: {
363
+ protocolVersion: params?.protocolVersion ?? "2024-11-05",
364
+ capabilities: { tools: {} },
365
+ serverInfo: { name: "boardui", version: VERSION },
366
+ },
367
+ });
368
+ }
369
+ if (
370
+ method === "notifications/initialized" ||
371
+ method === "notifications/cancelled"
372
+ )
373
+ return;
374
+ if (method === "ping") return write({ jsonrpc: "2.0", id, result: {} });
375
+ if (method === "tools/list")
376
+ return write({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
377
+ if (method === "tools/call") {
378
+ try {
379
+ const result = await call(
380
+ params.name,
381
+ params.arguments ?? {},
382
+ serverKey
383
+ );
384
+ return write({
385
+ jsonrpc: "2.0",
386
+ id,
387
+ result: { content: [{ type: "text", text: String(result) }] },
388
+ });
389
+ } catch (error) {
390
+ return write({
391
+ jsonrpc: "2.0",
392
+ id,
393
+ result: {
394
+ content: [{ type: "text", text: `Error: ${error.message}` }],
395
+ isError: true,
396
+ },
397
+ });
398
+ }
399
+ }
400
+ write({
401
+ jsonrpc: "2.0",
402
+ id,
403
+ error: { code: -32_601, message: `Method not found: ${method}` },
404
+ });
405
+ })().catch(() => {});
406
+ });
407
+ return new Promise(() => {}); // stdio server 常驻
408
+ }
package/src/project.js ADDED
@@ -0,0 +1,45 @@
1
+ // 项目检测复刻 — package manager / ts / 组件写入根 / 别名
2
+ import { existsSync, readFileSync } from "fs";
3
+ import path from "path";
4
+
5
+ export function detectProject(cwd = process.cwd()) {
6
+ const pkgPath = path.join(cwd, "package.json");
7
+ const pkg = existsSync(pkgPath)
8
+ ? JSON.parse(readFileSync(pkgPath, "utf8"))
9
+ : null;
10
+ const deps = {
11
+ ...(pkg?.dependencies ?? {}),
12
+ ...(pkg?.devDependencies ?? {}),
13
+ };
14
+
15
+ const usesSrcDir = existsSync(path.join(cwd, "src"));
16
+ const sourceRoot = usesSrcDir ? path.join(cwd, "src") : cwd;
17
+
18
+ // 组件目录: @/ 别名指向的 components 目录,fallback 到 <src>/components/ui
19
+ let alias = "@/components/ui";
20
+ try {
21
+ const ts = readFileSync(path.join(cwd, "tsconfig.json"), "utf8");
22
+ const m = ts.match(/"components\/\*"[^}]*"(.\/[^"]*components)[^"]*"/);
23
+ if (m) alias = m[1].replace(/^\.\//, "@/");
24
+ } catch {}
25
+
26
+ let packageManager = "npm";
27
+ if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
28
+ else if (existsSync(path.join(cwd, "yarn.lock"))) packageManager = "yarn";
29
+ else if (existsSync(path.join(cwd, "bun.lockb"))) packageManager = "bun";
30
+
31
+ return { cwd, pkg, deps, usesSrcDir, sourceRoot, alias, packageManager };
32
+ }
33
+
34
+ export function installCommand(pm, packages) {
35
+ switch (pm) {
36
+ case "pnpm":
37
+ return ["pnpm", ["add", ...packages]];
38
+ case "yarn":
39
+ return ["yarn", ["add", ...packages]];
40
+ case "bun":
41
+ return ["bun", ["add", ...packages]];
42
+ default:
43
+ return ["npm", ["install", ...packages]];
44
+ }
45
+ }
@@ -0,0 +1,54 @@
1
+ // Registry 拉取 — shadcn registry.json schema 兼容
2
+ // 公共: GET {BASE}/r/registry.json 与 /r/<name>.json (免鉴权)
3
+ // Pro: GET {BASE}/api/pro/r/<name> Bearer <key>(CLI `add --key` 传入 / MCP server `mcp --key` 启动内嵌;校验/缓存/熔断全在服务端)
4
+ // 端点内置,不提供 env 覆盖,本机不存任何状态。
5
+ const BASE = "https://boardui.collectui.pro";
6
+
7
+ export const registryBaseUrl = () => `${BASE}/r`;
8
+ export const proRegistryBaseUrl = () => `${BASE}/api/pro/r`;
9
+
10
+ async function fetchJson(url, headers) {
11
+ const response = await fetch(url, { headers });
12
+ if (!response.ok)
13
+ throw new Error(`Registry request failed (${response.status}) for ${url}`);
14
+ return response.json();
15
+ }
16
+
17
+ export async function fetchIndex(includePro = false) {
18
+ const base = await fetchJson(`${registryBaseUrl()}/registry.json`);
19
+ if (!includePro) return base;
20
+ try {
21
+ const pro = await fetchJson(`${registryBaseUrl()}/registry-pro.json`);
22
+ return {
23
+ ...base,
24
+ items: [...base.items, ...pro.items.map((i) => ({ ...i, pro: true }))],
25
+ };
26
+ } catch {
27
+ return base; // pro index 拉不到时静默降级
28
+ }
29
+ }
30
+
31
+ // 单个条目:先公共后 pro。pro 的 key 校验在服务端完成,客户端只管带 Bearer 下载。
32
+ export async function fetchItem(name, key) {
33
+ try {
34
+ return await fetchJson(`${registryBaseUrl()}/${name}.json`);
35
+ } catch {
36
+ if (!key) {
37
+ throw new Error(
38
+ `"${name}" is a BoardUI Pro component. Pass --key <license-key> to install it (CLI), or start the MCP server with --key <key>. The key is distributed by your platform or administrator.`
39
+ );
40
+ }
41
+ const response = await fetch(`${proRegistryBaseUrl()}/${name}`, {
42
+ headers: { Authorization: `Bearer ${key}` },
43
+ });
44
+ if (!response.ok) {
45
+ let message = `Pro registry request failed (${response.status}).`;
46
+ try {
47
+ const body = await response.json();
48
+ if (body.message ?? body.error) message = body.message ?? body.error;
49
+ } catch {}
50
+ throw new Error(message);
51
+ }
52
+ return response.json();
53
+ }
54
+ }
package/src/rules.js ADDED
@@ -0,0 +1,53 @@
1
+ // Agent 设计规则 — 与 boardui 的 applyAgentRules 同款:
2
+ // rules registry 条目 → AGENTS.md fenced section + .cursor/rules/boardui.mdc
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import path from "path";
5
+ import { fetchItem } from "./registry.js";
6
+
7
+ export async function applyAgentRules(projectDir) {
8
+ const rules = await fetchItem("rules");
9
+ const content = rules.files?.[0]?.content;
10
+ if (!content) return ["Rules item has no content."];
11
+
12
+ const agentsMd = path.join(projectDir, "AGENTS.md");
13
+ const section = `\n<!-- boardui:rules start -->\n${content}\n<!-- boardui:rules end -->\n`;
14
+ let verb;
15
+ if (existsSync(agentsMd)) {
16
+ const prev = readFileSync(agentsMd, "utf8");
17
+ if (
18
+ prev.includes("boardui:rules start") ||
19
+ prev.includes("bdui2:rules start")
20
+ ) {
21
+ writeFileSync(
22
+ agentsMd,
23
+ prev.replace(
24
+ /<!-- boardui:rules start -->[\s\S]*?<!-- boardui:rules end -->/,
25
+ section.trimEnd()
26
+ )
27
+ );
28
+ verb = "refreshed in AGENTS.md";
29
+ } else {
30
+ writeFileSync(agentsMd, prev + section);
31
+ verb = "appended to AGENTS.md";
32
+ }
33
+ } else {
34
+ writeFileSync(agentsMd, `# AGENTS.md\n${section}`);
35
+ verb = "written to a new AGENTS.md";
36
+ }
37
+
38
+ const cursorDir = path.join(projectDir, ".cursor", "rules");
39
+ mkdirSync(cursorDir, { recursive: true });
40
+ writeFileSync(path.join(cursorDir, "boardui.mdc"), content);
41
+
42
+ const lines = [` rules ${verb}(AGENTS.md + .cursor/rules/boardui.mdc)`];
43
+ const claudeMd = path.join(projectDir, "CLAUDE.md");
44
+ if (
45
+ existsSync(claudeMd) &&
46
+ !readFileSync(claudeMd, "utf8").includes("AGENTS.md")
47
+ ) {
48
+ lines.push(
49
+ ' Note: CLAUDE.md does not reference AGENTS.md — add a "@AGENTS.md" line so Claude Code loads the BoardUI rules too.'
50
+ );
51
+ }
52
+ return lines;
53
+ }
package/src/skill.js ADDED
@@ -0,0 +1,38 @@
1
+ // `bdui skill` — 从 registry 拉 skill 条目(SKILL.md + references)写入目标目录
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
+ import path from "path";
4
+ import c from "./color.js";
5
+ import { fetchItem } from "./registry.js";
6
+
7
+ export async function cmdSkill(flags) {
8
+ // -g → ~/.claude/skills; -c → ~/.agents/skills (codex/gemini);默认项目内 .claude/skills
9
+ const home = process.env.HOME;
10
+ let target;
11
+ if (flags.global) target = path.join(home, ".claude", "skills", "boardui");
12
+ else if (flags.client)
13
+ target = path.join(home, ".agents", "skills", "boardui");
14
+ else target = path.join(process.cwd(), ".claude", "skills", "boardui");
15
+
16
+ const skill = await fetchItem("skill");
17
+ if (existsSync(target) && !flags.force) {
18
+ throw new Error(
19
+ `${target} already exists — pass --force to overwrite (how you update).`
20
+ );
21
+ }
22
+ rmSync(target, { recursive: true, force: true });
23
+ mkdirSync(target, { recursive: true });
24
+ let n = 0;
25
+ for (const f of skill.files ?? []) {
26
+ const rel = f.path.includes("references/")
27
+ ? path.join("references", path.posix.basename(f.path))
28
+ : path.posix.basename(f.path);
29
+ const dest = path.join(target, rel);
30
+ mkdirSync(path.dirname(dest), { recursive: true });
31
+ writeFileSync(dest, f.content ?? "");
32
+ n++;
33
+ }
34
+ console.log(c.green(`✓ Agent skill installed → ${target} (${n} files)`));
35
+ console.log(
36
+ " Agents can now browse BoardUI with always-loaded design-system knowledge."
37
+ );
38
+ }
package/src/write.js ADDED
@@ -0,0 +1,51 @@
1
+ // 条目落盘共享逻辑(add / init 复用):registry item → sourceRoot 相对路径
2
+ // 已存在文件:--overwrite 覆盖、--yes 跳过、否则 prompts 交互询问(boardui 同款)
3
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
4
+ import path from "path";
5
+ import pc from "picocolors";
6
+ import prompts from "prompts";
7
+
8
+ /** 非交互环境(MCP/无 TTY/-y)返回 false(跳过);交互时逐个问,可 All */
9
+ async function confirmOverwrite(target, flags, state) {
10
+ if (flags.overwrite || state.all) return true;
11
+ if (flags.yes || !process.stdin.isTTY) return false;
12
+ const { value } = await prompts({
13
+ type: "select",
14
+ name: "value",
15
+ message: `Overwrite ${path.relative(process.cwd(), target)}?`,
16
+ choices: [
17
+ { title: "Yes", value: "yes" },
18
+ { title: "No (skip)", value: "no" },
19
+ { title: "All (overwrite the rest)", value: "all" },
20
+ ],
21
+ initial: 1,
22
+ stdout: process.stderr, // 交互走 stderr,不污染管道输出
23
+ });
24
+ if (value === "all") state.all = true;
25
+ return value === "yes" || value === "all";
26
+ }
27
+
28
+ export async function writeItemFiles(item, outRoot, flags = {}) {
29
+ let wrote = 0;
30
+ let wroteStyles = false;
31
+ const state = { all: false };
32
+ for (const file of item.files ?? []) {
33
+ const target = path.join(outRoot, file.path);
34
+ if (existsSync(target) && !(await confirmOverwrite(target, flags, state))) {
35
+ console.log(
36
+ ` ${pc.yellow("skip")} ${path.relative(process.cwd(), target)} (exists)`
37
+ );
38
+ continue;
39
+ }
40
+ mkdirSync(path.dirname(target), { recursive: true });
41
+ writeFileSync(target, file.content ?? "");
42
+ wrote++;
43
+ if (file.path.startsWith("styles/")) wroteStyles = true;
44
+ console.log(
45
+ ` ${pc.green("add")} ${path.relative(process.cwd(), target)}${item.pro ? pc.cyan(" (pro)") : ""}`
46
+ );
47
+ }
48
+ writeItemFiles.lastWroteStyles =
49
+ writeItemFiles.lastWroteStyles || wroteStyles;
50
+ return wrote;
51
+ }