@collectui/bdui 0.1.9 → 0.1.10

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.10",
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,22 @@
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 16
15
+ // Cache Components 预渲染会炸,官方目前 calendar 系如此
16
+ if (/\bnew Date\(\)|\.toDate\(/.test(src)) {
17
+ hints.push(
18
+ "Note: this item reads the current time while rendering (per current registry source). On Next.js 16 Cache Components, wrap it in <Suspense> or `next build` prerendering fails."
19
+ );
20
+ }
21
+ return hints;
22
+ }
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,79 @@ 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 oldIsTw =
186
+ existsSync(oldGlobals) &&
187
+ readFileSync(oldGlobals, "utf8").includes('@import "tailwindcss"');
188
+ if (oldIdx !== -1 && oldIsTw) {
189
+ lines[oldIdx] = 'import "@/styles/globals.css";';
190
+ console.log(
191
+ ` ${c.green("add")} ${rel}: replaced "./globals.css" → "@/styles/globals.css" (old file was a second Tailwind entry; merge custom styles into BoardUI's if needed)`
192
+ );
193
+ } else {
194
+ // 插到最后一个顶层 import 之后;没有 import 则插到文件头
195
+ let at = 0;
196
+ for (let i = 0; i < lines.length; i++)
197
+ if (/^import[\s{"']/.test(lines[i])) at = i + 1;
198
+ lines.splice(at, 0, 'import "@/styles/globals.css";');
199
+ console.log(
200
+ ` ${c.green("add")} ${rel}: import "@/styles/globals.css"`
201
+ );
202
+ }
203
+ content = lines.join("\n");
204
+ }
205
+ stylesWired = true;
206
+ // 字体接线(r2 P4):theme.css 期望 --font-inter/--font-mono-source 由
207
+ // next/font 暴露,模板默认 Geist 不定义它们,注入 Inter + JetBrains Mono
208
+ if (!content.includes("--font-inter")) {
209
+ const lines = content.split("\n");
161
210
  let at = 0;
162
211
  for (let i = 0; i < lines.length; i++)
163
212
  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;
213
+ lines.splice(
214
+ at,
215
+ 0,
216
+ 'import { Inter, JetBrains_Mono } from "next/font/google";',
217
+ 'const bduiInter = Inter({ subsets: ["latin"], variable: "--font-inter" });',
218
+ 'const bduiMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono-source" });'
219
+ );
220
+ let next = lines.join("\n");
221
+ const vars = "${bduiInter.variable} ${bduiMono.variable}";
222
+ const tag = next.match(/<html[^>]*>/);
223
+ if (!tag) {
224
+ fontsHint =
225
+ "could not find <html> in the layout — attach bduiInter/bduiMono variable classNames manually";
226
+ } else if (tag[0].includes("className={`")) {
227
+ next = next.replace("className={`", `className={\`${vars} `);
228
+ } else {
229
+ const cm = tag[0].match(/className="([^"]*)"/);
230
+ if (cm) {
231
+ next = next.replace(
232
+ `className="${cm[1]}"`,
233
+ "className={`" + (cm[1] ? `${cm[1]} ` : "") + vars + "`}"
234
+ );
235
+ } else {
236
+ next = next.replace(
237
+ tag[0],
238
+ tag[0].replace(/>$/, ` className={\`${vars}\`}>`)
239
+ );
240
+ }
241
+ }
242
+ content = next;
167
243
  console.log(
168
- ` ${c.green("add")} ${path.relative(cwd, layoutFile)}: import "@/styles/globals.css"`
244
+ ` ${c.green("add")} ${rel}: Inter + JetBrains Mono via next/font (--font-inter / --font-mono-source)`
169
245
  );
170
246
  }
247
+ writeFileSync(layoutFile, content);
171
248
  }
172
249
 
173
250
  // agent 规则(AGENTS.md + .cursor/rules/boardui.mdc)
@@ -190,13 +267,18 @@ export async function cmdInit(flags) {
190
267
  throw e;
191
268
  }
192
269
  }
270
+ // relative 同目录返回 ""/拼接句号曾显示成 "written to .."(盲测 r2 P10)
271
+ const dest = path.relative(cwd, outRoot);
193
272
  console.log(
194
273
  c.green(
195
- `\n✓ ${wrote} foundation file(s) written to ${path.relative(cwd, outRoot) || "."}.`
274
+ `\n✓ ${wrote} foundation file(s) written to ${
275
+ dest === "" || dest === "." ? "the project root" : dest
276
+ }.`
196
277
  )
197
278
  );
198
279
  for (const note of setup.notes) console.log(c.yellow(`\n${note}\n`));
199
280
  for (const line of ruleLines) console.log(line);
281
+ if (fontsHint) console.log(c.yellow(` warn fonts: ${fontsHint}`));
200
282
  console.log(
201
283
  stylesWired
202
284
  ? ` ${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
  }