@collectui/bdui 0.1.9 → 0.1.11

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.9",
3
+ "version": "0.1.11",
4
4
  "description": "Add BoardUI components to your project.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/add.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { spawn } from "child_process";
4
4
  import ora from "ora";
5
5
  import c from "./color.js";
6
+ import { itemHints } from "./hints.js";
6
7
  import { detectProject, installCommand } from "./project.js";
7
8
  import { fetchIndex, fetchItem } from "./registry.js";
8
9
  import { writeItemFiles } from "./write.js";
@@ -95,6 +96,7 @@ export async function cmdAdd(names, flags) {
95
96
  const deps = new Map(); // pkgName → 带 版本的 spec(去重,优先带版本形式)
96
97
  const nextImports = new Set();
97
98
  let wrote = 0;
99
+ let skippedExisting = 0;
98
100
  writeItemFiles.lastWroteStyles = false;
99
101
 
100
102
  for (const item of resolved.values()) {
@@ -110,7 +112,12 @@ export async function cmdAdd(names, flags) {
110
112
  for (const m of (f.content ?? "").matchAll(/from\s+["'](next\/[a-z-]+)/g))
111
113
  nextImports.add(m[1]);
112
114
  }
113
- wrote += await writeItemFiles(item, outRoot, flags);
115
+ const r = await writeItemFiles(item, outRoot, flags);
116
+ wrote += r.wrote;
117
+ skippedExisting += r.skippedExisting;
118
+ // 源码特征提示(不写死:特征随官方源码演进,官方修好提示自动消失)
119
+ for (const h of itemHints(item))
120
+ console.log(c.yellow(` ${item.name}: ${h}`));
114
121
  }
115
122
 
116
123
  if (deps.size) {
@@ -139,11 +146,20 @@ export async function cmdAdd(names, flags) {
139
146
  }
140
147
  console.log(c.green(`\n✓ ${wrote} file(s) written.`));
141
148
  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
- );
149
+ if (skippedExisting > 0) {
150
+ // 全部跳过=已装过,不是上游坏了(盲测 r2 P7 误报「may be broken」)
151
+ console.log(
152
+ c.yellow(
153
+ ` already installed (${skippedExisting} file(s) exist) — pass --overwrite to update`
154
+ )
155
+ );
156
+ } else {
157
+ console.log(
158
+ c.yellow(
159
+ " warn nothing was written — every file was skipped, or the registry entry ships no component source (upstream entry may be broken)"
160
+ )
161
+ );
162
+ }
147
163
  }
148
164
  // Next 项目里 next/link 是一等公民,提示反而是噪音(盲测 #10)
149
165
  if (nextImports.size && !project.deps?.next) {
package/src/hints.js ADDED
@@ -0,0 +1,23 @@
1
+ // 条目提示(2026-10-26 盲测 r2 P2)——基于内容特征检测,不写死条目名:
2
+ // 官方源码随时会变,特征还在才提示,官方修好提示自动消失(用户铁律:
3
+ // 官方源码透传不改写,上游问题顶多提示,且提示不能写死)。
4
+
5
+ /**
6
+ * 按条目当前源码特征给出安装/使用提示;无特征命中返回 []。
7
+ * @param {object} item registry 条目(files[].content)
8
+ * @returns {string[]}
9
+ */
10
+ export function itemHints(item) {
11
+ const hints = [];
12
+ const src = (item.files ?? []).map((f) => f.content ?? "").join("\n");
13
+
14
+ // render 期读当前时间(new Date() / react-aria .toDate()):在 Next.js
15
+ // Cache Components 下,这类组件参与 prerender 时可能炸 build(是否炸取
16
+ // 决于该组件是否在预渲染路径渲染——实测未渲染于预渲染页时不炸)
17
+ if (/\bnew Date\(\)|\.toDate\(/.test(src)) {
18
+ hints.push(
19
+ "Note: this item reads the current time while rendering (per current registry source). Under Next.js Cache Components it can fail `next build` prerendering when it renders during prerender — wrap in <Suspense> if that happens (or preemptively)."
20
+ );
21
+ }
22
+ return hints;
23
+ }
package/src/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // `bdui init` — 对齐 boardui init:装 foundations(cx/theme/typography/globals)
2
2
  // + agent 规则(AGENTS.md + .cursor/rules),内容全部来自 registry(与官方同源)
3
3
 
4
+ import { execSync } from "child_process";
4
5
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
5
6
  import ora from "ora";
6
7
  import path from "path";
@@ -32,21 +33,27 @@ function frameworkSetup(project, cwd) {
32
33
  // Next.js:官方模板自带 @/ alias 与 @types/node;仅补缺失的 tailwind 管道。
33
34
  // postcss 配置是硬前提:模板没带 Tailwind 时 postcss.config 不存在,
34
35
  // @tailwindcss/postcss 装了也不接管,组件样式静默失效(next build 只给
35
- // "Unknown at rule: @apply" 警告仍报成功)——缺配置就补 v4 标准形态
36
- if (!has("@tailwindcss/postcss")) pkgs.push("@tailwindcss/postcss");
37
- const hasPostcssCfg = [
38
- "postcss.config.mjs",
39
- "postcss.config.js",
40
- "postcss.config.cjs",
41
- "postcss.config.ts",
42
- ].some((f) => existsSync(path.join(cwd, f)));
36
+ // "Unknown at rule: @apply" 警告仍报成功)——缺配置就补 v4 标准形态。
37
+ // 反向同理(CNA 16 默认 @tailwindcss/turbopack 管道):已走 turbopack
38
+ // 就不再注入 postcss,避免双管道并存困惑(盲测 r2 P13)
39
+ const hasTurbopackTw = has("@tailwindcss/turbopack");
40
+ if (!(hasTurbopackTw || has("@tailwindcss/postcss")))
41
+ pkgs.push("@tailwindcss/postcss");
43
42
  const writes = [];
44
- if (!hasPostcssCfg)
45
- writes.push({
46
- file: "postcss.config.mjs",
47
- content:
48
- 'const config = {\n plugins: { "@tailwindcss/postcss": {} },\n};\n\nexport default config;\n',
49
- });
43
+ if (!hasTurbopackTw) {
44
+ const hasPostcssCfg = [
45
+ "postcss.config.mjs",
46
+ "postcss.config.js",
47
+ "postcss.config.cjs",
48
+ "postcss.config.ts",
49
+ ].some((f) => existsSync(path.join(cwd, f)));
50
+ if (!hasPostcssCfg)
51
+ writes.push({
52
+ file: "postcss.config.mjs",
53
+ content:
54
+ 'const config = {\n plugins: { "@tailwindcss/postcss": {} },\n};\n\nexport default config;\n',
55
+ });
56
+ }
50
57
  return { pkgs, notes, writes };
51
58
  }
52
59
  const viteConfig = [
@@ -85,9 +92,9 @@ export async function cmdInit(flags) {
85
92
  throw new Error("No package.json found — run inside your app project.");
86
93
 
87
94
  // alias 不在此行展示:tsconfig 的 @/ 别名与实际落位(components/base/<group>)
88
- // 口径不同,曾让客户误读(2026-10-26 盲测 #6);落位在 ✓ 行明示
95
+ // 口径不同,曾让客户误读(盲测 #6);落位在 ✓ 行明示。src= 是内部术语,给人话(r2 P10)
89
96
  console.log(
90
- `${c.cyan("init")} detected ${project.packageManager}, src=${project.usesSrcDir}`
97
+ `${c.cyan("init")} detected ${project.packageManager}, source root: ${project.usesSrcDir ? "src/" : "project root"}`
91
98
  );
92
99
 
93
100
  // npm 依赖:foundation 用到的 + 可选 pro 依赖(官方 --pro 同款集)+ 框架适配
@@ -113,7 +120,7 @@ export async function cmdInit(flags) {
113
120
  deps.set(pkgName, dep);
114
121
  }
115
122
  }
116
- wrote += await writeItemFiles(item, outRoot, flags);
123
+ wrote += (await writeItemFiles(item, outRoot, flags)).wrote;
117
124
  }
118
125
 
119
126
  // 框架管道配置(postcss.config.mjs 等):样式静默失效的根因修复(盲测 #1)
@@ -139,9 +146,23 @@ export async function cmdInit(flags) {
139
146
  ` ${c.green("add")} .gitignore += .mcp.json (license key leak guard)`
140
147
  );
141
148
  }
149
+ // 已跟踪的 .mcp.json:gitignore 挡不住,防"已安全"错觉(盲测 r2 P8)
150
+ try {
151
+ execSync("git ls-files --error-unmatch .mcp.json", {
152
+ cwd,
153
+ stdio: "ignore",
154
+ });
155
+ console.log(
156
+ c.yellow(
157
+ " warn .mcp.json is already tracked by git — .gitignore does not untrack it. Run: git rm --cached .mcp.json (and rotate the key if it was ever pushed)"
158
+ )
159
+ );
160
+ } catch {}
142
161
  }
143
162
 
144
- // 样式入口自动接线:根布局存在且未引入时补 import,免手动 Note(盲测 #7)
163
+ // 样式入口接线:优先「替换」模板旧 globals 导入而非并排追加——两份
164
+ // @import \"tailwindcss\" = Tailwind 双编译(~75KB 冗余 CSS)+旧 body Arial
165
+ // 覆盖 BoardUI 排版(盲测 r2 P3);旧文件非 Tailwind 入口才走追加(盲测 #7)
145
166
  const layoutFile = [
146
167
  "app/layout.tsx",
147
168
  "app/layout.ts",
@@ -151,23 +172,101 @@ export async function cmdInit(flags) {
151
172
  .map((f) => path.join(project.sourceRoot, f))
152
173
  .find((p) => existsSync(p));
153
174
  let stylesWired = false;
175
+ let fontsHint = "";
154
176
  if (layoutFile) {
155
- const src = readFileSync(layoutFile, "utf8");
156
- if (src.includes("@/styles/globals.css")) {
157
- stylesWired = true;
158
- } else {
159
- const lines = src.split("\n");
160
- // 插到最后一个顶层 import 之后;没有 import 则插到文件头
177
+ const rel = path.relative(cwd, layoutFile);
178
+ let content = readFileSync(layoutFile, "utf8");
179
+ if (!content.includes("@/styles/globals.css")) {
180
+ const lines = content.split("\n");
181
+ const oldIdx = lines.findIndex((l) =>
182
+ /^\s*import\s+["']\.\/globals\.css["'];?\s*$/.test(l)
183
+ );
184
+ const oldGlobals = path.join(project.sourceRoot, "app/globals.css");
185
+ const oldContent = existsSync(oldGlobals)
186
+ ? readFileSync(oldGlobals, "utf8")
187
+ : "";
188
+ // 接管条件(r3 🔴 实测:no-tailwind CNA 的 body Arial 直接声明压过
189
+ // html 继承的 --font-inter,只追加不接管=全站 Arial):旧 globals 是
190
+ // Tailwind 入口或 CNA 默认存量(--background/--foreground 签名,含
191
+ // tailwind/no-tailwind 两形态)即替换;不像模板的自定义文件才追加
192
+ const oldIsTw = oldContent.includes('@import "tailwindcss"');
193
+ const oldIsCnaStock =
194
+ oldContent.includes("--background") &&
195
+ oldContent.includes("--foreground");
196
+ if (oldIdx !== -1 && (oldIsTw || oldIsCnaStock)) {
197
+ lines[oldIdx] = 'import "@/styles/globals.css";';
198
+ console.log(
199
+ ` ${c.green("add")} ${rel}: replaced "./globals.css" → "@/styles/globals.css" (old file was a stock scaffold stylesheet whose body rules would override BoardUI; merge any custom styles into BoardUI's)`
200
+ );
201
+ } else {
202
+ // 插到最后一个顶层 import 之后;没有 import 则插到文件头
203
+ let at = 0;
204
+ for (let i = 0; i < lines.length; i++)
205
+ if (/^import[\s{"']/.test(lines[i])) at = i + 1;
206
+ lines.splice(at, 0, 'import "@/styles/globals.css";');
207
+ console.log(
208
+ ` ${c.green("add")} ${rel}: import "@/styles/globals.css"`
209
+ );
210
+ }
211
+ content = lines.join("\n");
212
+ }
213
+ stylesWired = true;
214
+ // 字体接线(r2 P4):theme.css 期望 --font-inter/--font-mono-source 由
215
+ // next/font 暴露,模板默认 Geist 不定义它们,注入 Inter + JetBrains Mono
216
+ if (!content.includes("--font-inter")) {
217
+ // 清理模板残留的 Geist 字体(注入 Inter 后纯属浪费带宽,r3 伴随问题)
218
+ const geistImport =
219
+ 'import { Geist, Geist_Mono } from "next/font/google";';
220
+ if (content.includes(geistImport) && content.includes("geistSans")) {
221
+ content = content
222
+ .replace(geistImport + "\n", "")
223
+ .replace(/const geistSans = Geist\(\{[\s\S]*?\}\);?\n+/, "")
224
+ .replace(/const geistMono = Geist_Mono\(\{[\s\S]*?\}\);?\n+/, "")
225
+ .replace(/\$\{geistSans\.variable\}\s*/g, "")
226
+ .replace(/\$\{geistMono\.variable\}\s*/g, "");
227
+ console.log(
228
+ ` ${c.green("add")} ${rel}: removed leftover Geist fonts (Inter takes over)`
229
+ );
230
+ }
231
+ const lines = content.split("\n");
161
232
  let at = 0;
162
233
  for (let i = 0; i < lines.length; i++)
163
234
  if (/^import[\s{"']/.test(lines[i])) at = i + 1;
164
- lines.splice(at, 0, 'import "@/styles/globals.css";');
165
- writeFileSync(layoutFile, lines.join("\n"));
166
- stylesWired = true;
235
+ lines.splice(
236
+ at,
237
+ 0,
238
+ 'import { Inter, JetBrains_Mono } from "next/font/google";',
239
+ 'const bduiInter = Inter({ subsets: ["latin"], variable: "--font-inter" });',
240
+ 'const bduiMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono-source" });'
241
+ );
242
+ let next = lines.join("\n");
243
+ const vars = "${bduiInter.variable} ${bduiMono.variable}";
244
+ const tag = next.match(/<html[^>]*>/);
245
+ if (!tag) {
246
+ fontsHint =
247
+ "could not find <html> in the layout — attach bduiInter/bduiMono variable classNames manually";
248
+ } else if (tag[0].includes("className={`")) {
249
+ next = next.replace("className={`", `className={\`${vars} `);
250
+ } else {
251
+ const cm = tag[0].match(/className="([^"]*)"/);
252
+ if (cm) {
253
+ next = next.replace(
254
+ `className="${cm[1]}"`,
255
+ "className={`" + (cm[1] ? `${cm[1]} ` : "") + vars + "`}"
256
+ );
257
+ } else {
258
+ next = next.replace(
259
+ tag[0],
260
+ tag[0].replace(/>$/, ` className={\`${vars}\`}>`)
261
+ );
262
+ }
263
+ }
264
+ content = next;
167
265
  console.log(
168
- ` ${c.green("add")} ${path.relative(cwd, layoutFile)}: import "@/styles/globals.css"`
266
+ ` ${c.green("add")} ${rel}: Inter + JetBrains Mono via next/font (--font-inter / --font-mono-source)`
169
267
  );
170
268
  }
269
+ writeFileSync(layoutFile, content);
171
270
  }
172
271
 
173
272
  // agent 规则(AGENTS.md + .cursor/rules/boardui.mdc)
@@ -190,13 +289,18 @@ export async function cmdInit(flags) {
190
289
  throw e;
191
290
  }
192
291
  }
292
+ // relative 同目录返回 ""/拼接句号曾显示成 "written to .."(盲测 r2 P10)
293
+ const dest = path.relative(cwd, outRoot);
193
294
  console.log(
194
295
  c.green(
195
- `\n✓ ${wrote} foundation file(s) written to ${path.relative(cwd, outRoot) || "."}.`
296
+ `\n✓ ${wrote} foundation file(s) written to ${
297
+ dest === "" || dest === "." ? "the project root" : dest
298
+ }.`
196
299
  )
197
300
  );
198
301
  for (const note of setup.notes) console.log(c.yellow(`\n${note}\n`));
199
302
  for (const line of ruleLines) console.log(line);
303
+ if (fontsHint) console.log(c.yellow(` warn fonts: ${fontsHint}`));
200
304
  console.log(
201
305
  stylesWired
202
306
  ? ` ${c.dim('Note: BoardUI styles wired into your root layout (import "@/styles/globals.css").')}`
package/src/mcp.js CHANGED
@@ -8,6 +8,7 @@ import path from "path";
8
8
  import { createInterface } from "readline";
9
9
  import { z } from "zod";
10
10
  import { cmdAdd } from "./add.js";
11
+ import { itemHints } from "./hints.js";
11
12
  import { detectProject } from "./project.js";
12
13
  import { fetchIndex, fetchItem } from "./registry.js";
13
14
 
@@ -184,16 +185,40 @@ async function dispatch(name, args, captured, serverKey) {
184
185
  `internal dependencies: ${(item.registryDependencies ?? []).join(", ") || "none"}`,
185
186
  `files: ${(item.files ?? []).map((f) => f.path).join(", ")}`,
186
187
  ];
187
- const first = item.files?.[0];
188
- if (first?.content) {
189
- lines.push(
190
- "",
191
- "## Source (first file)",
192
- "```tsx",
193
- first.content,
194
- "```"
195
- );
188
+ // 全量源码输出(盲测 r2 P6:工具描述承诺 every file 却只回第一个)。
189
+ // 超大组件(整页模板)按文件逐个给,累计超限后剩余只列路径——
190
+ // 需要时 agent 可 install_components 落盘后再读文件。
191
+ const SRC_BUDGET = 150_000;
192
+ let used = 0;
193
+ for (const f of item.files ?? []) {
194
+ if (!f.content) continue;
195
+ if (used + f.content.length > SRC_BUDGET) {
196
+ lines.push(
197
+ "",
198
+ `(remaining files omitted for size: ${
199
+ (item.files ?? [])
200
+ .filter(
201
+ (x) =>
202
+ x.content &&
203
+ x !== f &&
204
+ used + x.content?.length > SRC_BUDGET
205
+ )
206
+ .map((x) => x.path)
207
+ .join(", ") || f.path
208
+ } — install_components writes them to disk)`
209
+ );
210
+ break;
211
+ }
212
+ used += f.content.length;
213
+ const lang = (f.path ?? "").endsWith(".ts")
214
+ ? "ts"
215
+ : (f.path ?? "").endsWith(".css")
216
+ ? "css"
217
+ : "tsx";
218
+ lines.push("", `## Source: ${f.path}`, "```" + lang, f.content, "```");
196
219
  }
220
+ // 源码特征提示(内容检测,不写死;官方修好自动消失)
221
+ for (const h of itemHints(item)) lines.push("", `> ${h}`);
197
222
  return lines.filter(Boolean).join("\n");
198
223
  }
199
224
 
@@ -219,6 +244,8 @@ async function dispatch(name, args, captured, serverKey) {
219
244
  }
220
245
  if (!(meta.example || meta.snippets?.length))
221
246
  lines.push("", "No usage examples recorded for this item.");
247
+ // 源码特征提示(不写死条目名:特征随官方源码演进,官方修好自动消失)
248
+ for (const h of itemHints(item)) lines.push("", `> ${h}`);
222
249
  return lines.filter((x) => x !== undefined).join("\n");
223
250
  }
224
251
 
package/src/registry.js CHANGED
@@ -57,6 +57,9 @@ export async function fetchItem(name, key) {
57
57
  } catch {}
58
58
  throw new Error(message);
59
59
  }
60
- return response.json();
60
+ const item = await response.json();
61
+ // pro 通道拿到的条目缺 pro:true(服务端不回显)——补上,落盘输出标 (pro)(盲测 r2 P13)
62
+ item.pro = true;
63
+ return item;
61
64
  }
62
65
  }
package/src/write.js CHANGED
@@ -27,6 +27,7 @@ async function confirmOverwrite(target, flags, state) {
27
27
 
28
28
  export async function writeItemFiles(item, outRoot, flags = {}) {
29
29
  let wrote = 0;
30
+ let skippedExisting = 0;
30
31
  let wroteStyles = false;
31
32
  const state = { all: false };
32
33
  for (const file of item.files ?? []) {
@@ -39,6 +40,7 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
39
40
  }
40
41
  const target = path.join(outRoot, rel);
41
42
  if (existsSync(target) && !(await confirmOverwrite(target, flags, state))) {
43
+ skippedExisting++;
42
44
  console.log(
43
45
  ` ${pc.yellow("skip")} ${path.relative(process.cwd(), target)} (exists)`
44
46
  );
@@ -52,7 +54,32 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
52
54
  ` ${pc.green("add")} ${path.relative(process.cwd(), target)}${item.pro ? pc.cyan(" (pro)") : ""}`
53
55
  );
54
56
  }
57
+ // 二进制 assets(base64):Pro 模板的头像/logo 等图片资源(盲测 r2 P1)
58
+ for (const asset of item.assets ?? []) {
59
+ const rel = path.normalize(asset.path ?? "");
60
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
61
+ console.log(` ${pc.red("skip")} unsafe path in asset: ${asset.path}`);
62
+ continue;
63
+ }
64
+ const target = path.join(outRoot, "public", rel);
65
+ if (existsSync(target) && !(await confirmOverwrite(target, flags, state))) {
66
+ skippedExisting++;
67
+ console.log(
68
+ ` ${pc.yellow("skip")} ${path.relative(process.cwd(), target)} (exists)`
69
+ );
70
+ continue;
71
+ }
72
+ mkdirSync(path.dirname(target), { recursive: true });
73
+ writeFileSync(target, Buffer.from(asset.content ?? "", "base64"));
74
+ wrote++;
75
+ console.log(
76
+ ` ${pc.green("add")} ${path.relative(
77
+ process.cwd(),
78
+ target
79
+ )}${item.pro ? pc.cyan(" (pro)") : ""} (asset)`
80
+ );
81
+ }
55
82
  writeItemFiles.lastWroteStyles =
56
83
  writeItemFiles.lastWroteStyles || wroteStyles;
57
- return wrote;
84
+ return { wrote, skippedExisting };
58
85
  }