@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@collectui/bdui",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Add BoardUI components to your project.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- ? resolve()
23
- : reject(
24
- new Error(`${command} ${args.join(" ")} exited with code ${code}`)
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.split("@")[0] || 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 { installPackages } from "./add.js";
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
- const deps = new Set(["clsx", "tailwind-merge"]);
33
- if (flags.pro) {
34
- for (const d of PRO_EXTRA_DEPS) deps.add(d);
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.split("@")[0] || dep;
44
- if (!project.deps[pkgName]) deps.add(dep);
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.bdui2Type ?? "base";
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.bdui2Type ?? "base";
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
- const ruleLines = await applyAgentRules(projectDir);
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
- try {
384
- const result = await call(
385
- params.name,
386
- params.arguments ?? {},
387
- serverKey
388
- );
389
- return write({
390
- jsonrpc: "2.0",
391
- id,
392
- result: { content: [{ type: "text", text: String(result) }] },
393
- });
394
- } catch (error) {
395
- return write({
396
- jsonrpc: "2.0",
397
- id,
398
- result: {
399
- content: [{ type: "text", text: `Error: ${error.message}` }],
400
- isError: true,
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
- const target = path.join(outRoot, file.path);
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)`