@collectui/bdui 0.1.4 → 0.1.6
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/package.json +1 -1
- package/src/add.js +50 -9
- package/src/init.js +63 -10
- package/src/list.js +1 -1
- package/src/mcp.js +38 -24
- package/src/write.js +8 -1
package/package.json
CHANGED
package/src/add.js
CHANGED
|
@@ -7,23 +7,38 @@ import { detectProject, installCommand } from "./project.js";
|
|
|
7
7
|
import { fetchIndex, fetchItem } from "./registry.js";
|
|
8
8
|
import { writeItemFiles } from "./write.js";
|
|
9
9
|
|
|
10
|
+
/** 依赖 spec → 包名。scoped 包(@scope/name@^1.0.0)的首个 @ 属于包名,不是分隔符 */
|
|
11
|
+
export function pkgBaseName(dep) {
|
|
12
|
+
const i = dep.indexOf("@", 1);
|
|
13
|
+
return i === -1 ? dep : dep.slice(0, i);
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export async function installPackages(pm, packages, cwd) {
|
|
11
17
|
if (!packages.length) return;
|
|
12
18
|
const [command, args] = installCommand(pm, packages);
|
|
19
|
+
// stderr 管道捕获(只留尾部):失败时带诊断输出,成功时丢弃不污染终端
|
|
20
|
+
const stderrLines = [];
|
|
13
21
|
await new Promise((resolve, reject) => {
|
|
14
22
|
const child = spawn(command, args, {
|
|
15
23
|
cwd,
|
|
16
|
-
stdio: "ignore",
|
|
24
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
17
25
|
shell: process.platform === "win32",
|
|
18
26
|
});
|
|
27
|
+
child.stderr?.on("data", (d) => {
|
|
28
|
+
stderrLines.push(String(d));
|
|
29
|
+
if (stderrLines.length > 40) stderrLines.shift();
|
|
30
|
+
});
|
|
19
31
|
child.on("error", reject);
|
|
20
|
-
child.on("close", (code) =>
|
|
21
|
-
code === 0
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (code === 0) return resolve();
|
|
34
|
+
const tail = stderrLines.join("").trim().split("\n").slice(-8).join("\n");
|
|
35
|
+
reject(
|
|
36
|
+
new Error(
|
|
37
|
+
`${command} ${args.join(" ")} exited with code ${code}` +
|
|
38
|
+
(tail ? `\n${tail}` : "")
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
});
|
|
27
42
|
});
|
|
28
43
|
}
|
|
29
44
|
|
|
@@ -68,21 +83,33 @@ export async function cmdAdd(names, flags) {
|
|
|
68
83
|
`Unknown component(s): ${missing.join(", ")} — run \`bdui list\`.`
|
|
69
84
|
);
|
|
70
85
|
}
|
|
86
|
+
if (resolved.size > wanted.length) {
|
|
87
|
+
console.log(
|
|
88
|
+
c.dim(
|
|
89
|
+
` closure ${resolved.size} item(s) resolved from ${wanted.length} requested (registry dependencies)`
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
71
93
|
|
|
72
94
|
const outRoot = flags.path ?? project.sourceRoot;
|
|
73
95
|
const deps = new Map(); // pkgName → 带 版本的 spec(去重,优先带版本形式)
|
|
96
|
+
const nextImports = new Set();
|
|
74
97
|
let wrote = 0;
|
|
75
98
|
writeItemFiles.lastWroteStyles = false;
|
|
76
99
|
|
|
77
100
|
for (const item of resolved.values()) {
|
|
78
101
|
for (const dep of item.dependencies ?? []) {
|
|
79
|
-
const pkgName = dep
|
|
102
|
+
const pkgName = pkgBaseName(dep);
|
|
80
103
|
if (!project.deps[pkgName]) {
|
|
81
104
|
const prev = deps.get(pkgName);
|
|
82
105
|
if (!prev || (prev === pkgName && dep !== pkgName))
|
|
83
106
|
deps.set(pkgName, dep);
|
|
84
107
|
}
|
|
85
108
|
}
|
|
109
|
+
for (const f of item.files ?? []) {
|
|
110
|
+
for (const m of (f.content ?? "").matchAll(/from\s+["'](next\/[a-z-]+)/g))
|
|
111
|
+
nextImports.add(m[1]);
|
|
112
|
+
}
|
|
86
113
|
wrote += await writeItemFiles(item, outRoot, flags);
|
|
87
114
|
}
|
|
88
115
|
|
|
@@ -111,6 +138,20 @@ export async function cmdAdd(names, flags) {
|
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
console.log(c.green(`\n✓ ${wrote} file(s) written.`));
|
|
141
|
+
if (wrote === 0) {
|
|
142
|
+
console.log(
|
|
143
|
+
c.yellow(
|
|
144
|
+
" warn nothing was written — every file was skipped, or the registry entry ships no component source (upstream entry may be broken)"
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (nextImports.size) {
|
|
149
|
+
console.log(
|
|
150
|
+
c.yellow(
|
|
151
|
+
`\nNote: ${[...nextImports].sort().join(", ")} imported by installed files — these only compile in a Next.js project.`
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
114
155
|
// 与 boardui 同款 Note:样式入口提示(本次写过 styles/ 文件时)
|
|
115
156
|
if (writeItemFiles.lastWroteStyles) {
|
|
116
157
|
console.log(
|
package/src/init.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// `bdui init` — 对齐 boardui init:装 foundations(cx/theme/typography/globals)
|
|
2
2
|
// + agent 规则(AGENTS.md + .cursor/rules),内容全部来自 registry(与官方同源)
|
|
3
3
|
|
|
4
|
+
import { existsSync } from "fs";
|
|
4
5
|
import ora from "ora";
|
|
5
|
-
import
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { installPackages, pkgBaseName } from "./add.js";
|
|
6
8
|
import c from "./color.js";
|
|
7
9
|
import { detectProject } from "./project.js";
|
|
8
10
|
import { fetchItem } from "./registry.js";
|
|
@@ -18,6 +20,48 @@ const PRO_EXTRA_DEPS = [
|
|
|
18
20
|
"@internationalized/date",
|
|
19
21
|
];
|
|
20
22
|
|
|
23
|
+
/** BoardUI 按 Tailwind v4 + "@/..." alias 编写;官方按 Next.js 模板(自带基建),
|
|
24
|
+
* 其他框架 init 必须补齐,否则装完 build 全红(2026-10-03 Vite 实测 50+ 错)。 */
|
|
25
|
+
function frameworkSetup(project, cwd) {
|
|
26
|
+
const notes = [];
|
|
27
|
+
const pkgs = [];
|
|
28
|
+
const has = (n) => Boolean(project.deps[n]);
|
|
29
|
+
|
|
30
|
+
if (!has("tailwindcss")) pkgs.push("tailwindcss");
|
|
31
|
+
if (project.deps?.next) {
|
|
32
|
+
// Next.js:官方模板自带 @/ alias 与 @types/node;仅补缺失的 tailwind 管道
|
|
33
|
+
if (!has("@tailwindcss/postcss")) pkgs.push("@tailwindcss/postcss");
|
|
34
|
+
return { pkgs, notes };
|
|
35
|
+
}
|
|
36
|
+
const viteConfig = [
|
|
37
|
+
"vite.config.ts",
|
|
38
|
+
"vite.config.js",
|
|
39
|
+
"vite.config.mjs",
|
|
40
|
+
].find((f) => existsSync(path.join(cwd, f)));
|
|
41
|
+
const isVite = has("vite") || viteConfig != null;
|
|
42
|
+
if (isVite) {
|
|
43
|
+
if (!has("@tailwindcss/vite")) pkgs.push("@tailwindcss/vite");
|
|
44
|
+
if (existsSync(path.join(cwd, "tsconfig.json")) && !has("@types/node"))
|
|
45
|
+
pkgs.push("@types/node");
|
|
46
|
+
notes.push(
|
|
47
|
+
[
|
|
48
|
+
"Vite detected — finish the setup manually:",
|
|
49
|
+
" 1. Add the plugin & alias to " +
|
|
50
|
+
(viteConfig ?? "vite.config.ts") +
|
|
51
|
+
":",
|
|
52
|
+
' import tailwindcss from "@tailwindcss/vite";',
|
|
53
|
+
' import path from "node:path";',
|
|
54
|
+
" // plugins: [tailwindcss()]",
|
|
55
|
+
' // resolve: { alias: { "@": path.resolve(__dirname, "./src") } }',
|
|
56
|
+
' 2. Map the "@/..." alias in tsconfig (compilerOptions.paths):',
|
|
57
|
+
' { "paths": { "@/*": ["./src/*"] } }',
|
|
58
|
+
" (tsconfig uses references → add to tsconfig.app.json as well)",
|
|
59
|
+
].join("\n")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return { pkgs, notes };
|
|
63
|
+
}
|
|
64
|
+
|
|
21
65
|
export async function cmdInit(flags) {
|
|
22
66
|
const cwd = flags.dir ?? process.cwd();
|
|
23
67
|
const project = detectProject(cwd);
|
|
@@ -28,11 +72,15 @@ export async function cmdInit(flags) {
|
|
|
28
72
|
`${c.cyan("init")} detected ${project.packageManager}, src=${project.usesSrcDir}, alias=${project.alias}`
|
|
29
73
|
);
|
|
30
74
|
|
|
31
|
-
// npm 依赖:foundation 用到的 + 可选 pro 依赖(官方 --pro 同款集)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
75
|
+
// npm 依赖:foundation 用到的 + 可选 pro 依赖(官方 --pro 同款集)+ 框架适配
|
|
76
|
+
// Map 按 pkgName 去重(裸名与带版本 spec 共存会导致重复 install)
|
|
77
|
+
const baseDeps = ["clsx", "tailwind-merge"];
|
|
78
|
+
if (flags.pro) baseDeps.push(...PRO_EXTRA_DEPS);
|
|
79
|
+
const setup = frameworkSetup(project, cwd);
|
|
80
|
+
baseDeps.push(...setup.pkgs);
|
|
81
|
+
const deps = new Map(); // pkgName → spec(优先带版本形式)
|
|
82
|
+
for (const d of baseDeps)
|
|
83
|
+
if (!project.deps[pkgBaseName(d)]) deps.set(pkgBaseName(d), d);
|
|
36
84
|
|
|
37
85
|
// foundations 条目落盘(boardui init 同款:theme tokens、type scale、globals、cx)
|
|
38
86
|
const outRoot = flags.path ?? project.sourceRoot;
|
|
@@ -40,8 +88,12 @@ export async function cmdInit(flags) {
|
|
|
40
88
|
for (const name of FOUNDATIONS) {
|
|
41
89
|
const item = await fetchItem(name);
|
|
42
90
|
for (const dep of item.dependencies ?? []) {
|
|
43
|
-
const pkgName = dep
|
|
44
|
-
if (!project.deps[pkgName])
|
|
91
|
+
const pkgName = pkgBaseName(dep);
|
|
92
|
+
if (!project.deps[pkgName]) {
|
|
93
|
+
const prev = deps.get(pkgName);
|
|
94
|
+
if (!prev || (prev === pkgName && dep !== pkgName))
|
|
95
|
+
deps.set(pkgName, dep);
|
|
96
|
+
}
|
|
45
97
|
}
|
|
46
98
|
wrote += await writeItemFiles(item, outRoot, flags);
|
|
47
99
|
}
|
|
@@ -50,7 +102,7 @@ export async function cmdInit(flags) {
|
|
|
50
102
|
const ruleLines = await applyAgentRules(cwd);
|
|
51
103
|
|
|
52
104
|
if (deps.size) {
|
|
53
|
-
console.log(` ${c.cyan("deps")} ${[...deps].join(", ")}`);
|
|
105
|
+
console.log(` ${c.cyan("deps")} ${[...deps.values()].join(", ")}`);
|
|
54
106
|
const spin = process.stderr.isTTY
|
|
55
107
|
? ora({
|
|
56
108
|
text: "Installing dependencies…",
|
|
@@ -59,7 +111,7 @@ export async function cmdInit(flags) {
|
|
|
59
111
|
}).start()
|
|
60
112
|
: null;
|
|
61
113
|
try {
|
|
62
|
-
await installPackages(project.packageManager, [...deps], cwd);
|
|
114
|
+
await installPackages(project.packageManager, [...deps.values()], cwd);
|
|
63
115
|
spin?.succeed("Dependencies installed");
|
|
64
116
|
} catch (e) {
|
|
65
117
|
spin?.fail("Dependency installation failed");
|
|
@@ -67,6 +119,7 @@ export async function cmdInit(flags) {
|
|
|
67
119
|
}
|
|
68
120
|
}
|
|
69
121
|
console.log(c.green(`\n✓ ${wrote} foundation file(s) written.`));
|
|
122
|
+
for (const note of setup.notes) console.log(c.yellow(`\n${note}\n`));
|
|
70
123
|
for (const line of ruleLines) console.log(line);
|
|
71
124
|
console.log(
|
|
72
125
|
` ${c.dim('Note: import the styles in your root layout or global css: import "@/styles/globals.css";')}`
|
package/src/list.js
CHANGED
|
@@ -6,7 +6,7 @@ export async function cmdList() {
|
|
|
6
6
|
const { items } = await fetchIndex(true);
|
|
7
7
|
const groups = new Map();
|
|
8
8
|
for (const item of items) {
|
|
9
|
-
const key = item.
|
|
9
|
+
const key = item.boarduiType ?? "base";
|
|
10
10
|
if (!groups.has(key)) groups.set(key, []);
|
|
11
11
|
groups.get(key).push(item);
|
|
12
12
|
}
|
package/src/mcp.js
CHANGED
|
@@ -158,7 +158,7 @@ async function dispatch(name, args, captured, serverKey) {
|
|
|
158
158
|
const lines = [header];
|
|
159
159
|
const groups = new Map();
|
|
160
160
|
for (const i of items) {
|
|
161
|
-
const g = i.
|
|
161
|
+
const g = i.boarduiType ?? "base";
|
|
162
162
|
if (!groups.has(g)) groups.set(g, []);
|
|
163
163
|
groups.get(g).push(i);
|
|
164
164
|
}
|
|
@@ -240,14 +240,14 @@ async function dispatch(name, args, captured, serverKey) {
|
|
|
240
240
|
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.`;
|
|
241
241
|
}
|
|
242
242
|
const { cmdInit } = await import("./init.js");
|
|
243
|
+
// cmdInit 内部已应用 agent rules(init.js 的 applyAgentRules),此处不重复刷
|
|
243
244
|
await cmdInit({
|
|
244
245
|
dir: projectDir,
|
|
245
246
|
pro: args.pro,
|
|
246
247
|
overwrite: args.overwrite,
|
|
247
248
|
yes: true,
|
|
248
249
|
});
|
|
249
|
-
|
|
250
|
-
return ["Setup complete.", ...captured, ...ruleLines].join("\n");
|
|
250
|
+
return ["Setup complete.", ...captured].join("\n");
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
case "install_rules":
|
|
@@ -346,6 +346,18 @@ export async function cmdMcp(options = {}) {
|
|
|
346
346
|
const rl = createInterface({ input: process.stdin });
|
|
347
347
|
const write = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
|
|
348
348
|
|
|
349
|
+
// tools/call 串行队列:安装类工具写同一项目目录 + console.log 捕获是全局单例,
|
|
350
|
+
// 并发调用会互相踩(文件竞争 / 输出丢失)。其他方法即时响应不受队列影响。
|
|
351
|
+
let callQueue = Promise.resolve();
|
|
352
|
+
function serialize(fn) {
|
|
353
|
+
const run = callQueue.then(fn, fn);
|
|
354
|
+
callQueue = run.then(
|
|
355
|
+
() => {},
|
|
356
|
+
() => {}
|
|
357
|
+
);
|
|
358
|
+
return run;
|
|
359
|
+
}
|
|
360
|
+
|
|
349
361
|
rl.on("line", (line) => {
|
|
350
362
|
if (!line.trim()) return;
|
|
351
363
|
let msg;
|
|
@@ -380,27 +392,29 @@ export async function cmdMcp(options = {}) {
|
|
|
380
392
|
if (method === "tools/list")
|
|
381
393
|
return write({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
|
|
382
394
|
if (method === "tools/call") {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
395
|
+
return serialize(async () => {
|
|
396
|
+
try {
|
|
397
|
+
const result = await call(
|
|
398
|
+
params.name,
|
|
399
|
+
params.arguments ?? {},
|
|
400
|
+
serverKey
|
|
401
|
+
);
|
|
402
|
+
return write({
|
|
403
|
+
jsonrpc: "2.0",
|
|
404
|
+
id,
|
|
405
|
+
result: { content: [{ type: "text", text: String(result) }] },
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return write({
|
|
409
|
+
jsonrpc: "2.0",
|
|
410
|
+
id,
|
|
411
|
+
result: {
|
|
412
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
413
|
+
isError: true,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
});
|
|
404
418
|
}
|
|
405
419
|
write({
|
|
406
420
|
jsonrpc: "2.0",
|
package/src/write.js
CHANGED
|
@@ -30,7 +30,14 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
|
|
|
30
30
|
let wroteStyles = false;
|
|
31
31
|
const state = { all: false };
|
|
32
32
|
for (const file of item.files ?? []) {
|
|
33
|
-
|
|
33
|
+
// 路径守卫:file.path 不得逃逸 outRoot(防 ../ 遍历;信任官方 registry
|
|
34
|
+
// 前提下是纵深防御,与 shadcn 同水位再进一步)
|
|
35
|
+
const rel = path.normalize(file.path ?? "");
|
|
36
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
37
|
+
console.log(` ${pc.red("skip")} unsafe path in item: ${file.path}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const target = path.join(outRoot, rel);
|
|
34
41
|
if (existsSync(target) && !(await confirmOverwrite(target, flags, state))) {
|
|
35
42
|
console.log(
|
|
36
43
|
` ${pc.yellow("skip")} ${path.relative(process.cwd(), target)} (exists)`
|