@collectui/bdui 0.1.11 → 0.2.0
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 +11 -2
- package/src/auth.js +124 -0
- package/src/cli.js +18 -4
- package/src/mcp.js +13 -11
- package/src/registry.js +3 -3
- package/src/skill.js +2 -1
- package/src/write.js +14 -2
package/package.json
CHANGED
package/src/add.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import ora from "ora";
|
|
5
|
+
import { readSavedKey, resolveKey } from "./auth.js";
|
|
5
6
|
import c from "./color.js";
|
|
6
7
|
import { itemHints } from "./hints.js";
|
|
7
8
|
import { detectProject, installCommand } from "./project.js";
|
|
@@ -60,8 +61,8 @@ export async function cmdAdd(names, flags) {
|
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
// 递归收集:name → item(公共 + pro),依赖闭包先装;pro
|
|
64
|
-
const key = flags.key;
|
|
64
|
+
// 递归收集:name → item(公共 + pro),依赖闭包先装;pro 的 key 三级解析(flag/env/login 落盘)
|
|
65
|
+
const key = resolveKey(flags.key);
|
|
65
66
|
const resolved = new Map();
|
|
66
67
|
const missing = [];
|
|
67
68
|
async function collect(name) {
|
|
@@ -177,4 +178,12 @@ export async function cmdAdd(names, flags) {
|
|
|
177
178
|
)
|
|
178
179
|
);
|
|
179
180
|
}
|
|
181
|
+
// 显式 --key 且本机未存过:一次性引导 login(存过/已用 login 就不再唠叨)
|
|
182
|
+
if (flags.key && !readSavedKey()) {
|
|
183
|
+
console.log(
|
|
184
|
+
c.dim(
|
|
185
|
+
`\nTip: run \`bdui login --key <key>\` once — future commands won't need --key on this machine.`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
180
189
|
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// `bdui login/logout` — license key 落盘一次,后续命令自动携带(本机级,0600,不进任何项目仓库)
|
|
2
|
+
// 取 key 优先级:--key 显式 flag > BDUI_KEY env > login 落盘文件
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import prompts from "prompts";
|
|
14
|
+
import c from "./color.js";
|
|
15
|
+
import { proRegistryBaseUrl } from "./registry.js";
|
|
16
|
+
|
|
17
|
+
// 验证探针:借一个必不存在的条目名打 Pro 端点。服务端路由先鉴权(requireValidKey)
|
|
18
|
+
// 后查条目,所以 404 = key 已通过鉴权,无需下载任何真实条目(零开销验证)。
|
|
19
|
+
const PROBE_NAME = "license-check-probe";
|
|
20
|
+
|
|
21
|
+
export function licenseFile() {
|
|
22
|
+
const dir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
23
|
+
return path.join(dir, "bdui", "license.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readSavedKey() {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(licenseFile(), "utf8")).key ?? null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 统一取 key:flag(显式当次指定永远赢) > env(CI) > login 落盘 */
|
|
35
|
+
export function resolveKey(flagKey) {
|
|
36
|
+
return flagKey ?? process.env.BDUI_KEY ?? readSavedKey();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveKey(key) {
|
|
40
|
+
const file = licenseFile();
|
|
41
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
42
|
+
writeFileSync(
|
|
43
|
+
file,
|
|
44
|
+
`${JSON.stringify({ key, savedAt: new Date().toISOString() }, null, 2)}\n`
|
|
45
|
+
);
|
|
46
|
+
chmodSync(file, 0o600);
|
|
47
|
+
return file;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 401 = key 不存在/无效;423 = 暂停/封禁;429 = 限流;其余(5xx/网络)按"暂不可验证"处理
|
|
51
|
+
async function verifyKey(key) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`${proRegistryBaseUrl()}/${PROBE_NAME}`, {
|
|
54
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
55
|
+
});
|
|
56
|
+
if (res.status === 404 || res.status === 200) return { ok: true };
|
|
57
|
+
if (res.status === 401)
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
message: "Invalid license key — check it and try again.",
|
|
61
|
+
};
|
|
62
|
+
if (res.status === 423)
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
message:
|
|
66
|
+
"This key is suspended or banned — contact your platform or administrator.",
|
|
67
|
+
};
|
|
68
|
+
if (res.status === 429)
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
message: "Key check rate-limited — try again in a moment.",
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
message: `Could not verify the key (registry returned ${res.status}). Try again.`,
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
message: "Could not reach the registry to verify the key. Try again.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function cmdLogin(flags) {
|
|
86
|
+
let key = flags.key?.trim();
|
|
87
|
+
if (!key && process.stdin.isTTY) {
|
|
88
|
+
const r = await prompts({
|
|
89
|
+
type: "password",
|
|
90
|
+
name: "key",
|
|
91
|
+
message: "BoardUI license key:",
|
|
92
|
+
});
|
|
93
|
+
key = r?.key?.trim();
|
|
94
|
+
}
|
|
95
|
+
if (!key)
|
|
96
|
+
throw new Error("No key provided — run: bdui login --key <license-key>");
|
|
97
|
+
|
|
98
|
+
const replaced = Boolean(readSavedKey());
|
|
99
|
+
console.log(`${c.cyan("login")} verifying key…`);
|
|
100
|
+
const verdict = await verifyKey(key);
|
|
101
|
+
if (!verdict.ok) throw new Error(verdict.message);
|
|
102
|
+
|
|
103
|
+
const file = saveKey(key);
|
|
104
|
+
console.log(
|
|
105
|
+
c.green(
|
|
106
|
+
`✓ license key verified${replaced ? " (replaced the previously saved one)" : ""} and saved to ${file}`
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
console.log(
|
|
110
|
+
c.dim(
|
|
111
|
+
" add / skill / mcp now work without --key on this machine. Remove it anytime with: bdui logout"
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function cmdLogout() {
|
|
117
|
+
const file = licenseFile();
|
|
118
|
+
if (!existsSync(file)) {
|
|
119
|
+
console.log("No saved license key on this machine.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
rmSync(file);
|
|
123
|
+
console.log(c.green(`✓ saved license key removed (${file})`));
|
|
124
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { cmdAdd } from "./add.js";
|
|
5
|
+
import { cmdLogin, cmdLogout } from "./auth.js";
|
|
5
6
|
import { cmdInit } from "./init.js";
|
|
6
7
|
import { cmdList } from "./list.js";
|
|
7
8
|
import { cmdMcp } from "./mcp.js";
|
|
@@ -38,13 +39,26 @@ export async function run(argv) {
|
|
|
38
39
|
)
|
|
39
40
|
.action(cmdInit);
|
|
40
41
|
|
|
42
|
+
program
|
|
43
|
+
.command("login")
|
|
44
|
+
.description(
|
|
45
|
+
"Save your BoardUI license key once (verified, 0600, this machine only) — add/skill/mcp then work without --key."
|
|
46
|
+
)
|
|
47
|
+
.option("-k, --key <key>", "license key (prompts for it when omitted)")
|
|
48
|
+
.action(cmdLogin);
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command("logout")
|
|
52
|
+
.description("Remove the license key saved by `bdui login`.")
|
|
53
|
+
.action(cmdLogout);
|
|
54
|
+
|
|
41
55
|
program
|
|
42
56
|
.command("add [components...]")
|
|
43
57
|
.description("Add components to your project.")
|
|
44
58
|
.option("-a, --all", "add every component in the registry")
|
|
45
59
|
.option(
|
|
46
60
|
"-k, --key <key>",
|
|
47
|
-
"license key for Pro components (
|
|
61
|
+
"license key for Pro components (or run `bdui login` once to save it on this machine)"
|
|
48
62
|
)
|
|
49
63
|
.option(
|
|
50
64
|
"-p, --path <path>",
|
|
@@ -66,11 +80,11 @@ export async function run(argv) {
|
|
|
66
80
|
program
|
|
67
81
|
.command("mcp")
|
|
68
82
|
.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
|
|
83
|
+
"Start the BoardUI MCP server (stdio) so AI agents can browse and install components. Pass --key to unlock Pro items for the whole session, or run `bdui login` once and start it without."
|
|
70
84
|
)
|
|
71
85
|
.option(
|
|
72
86
|
"-k, --key <key>",
|
|
73
|
-
"license key for Pro components, active for this server session (
|
|
87
|
+
"license key for Pro components, active for this server session (or use the key saved by `bdui login`)"
|
|
74
88
|
)
|
|
75
89
|
.action((options) => cmdMcp(options));
|
|
76
90
|
|
|
@@ -91,7 +105,7 @@ export async function run(argv) {
|
|
|
91
105
|
.option("-f, --force", "overwrite an existing install (how you update)")
|
|
92
106
|
.option(
|
|
93
107
|
"-k, --key <key>",
|
|
94
|
-
"license key when the skill entry is Pro (
|
|
108
|
+
"license key when the skill entry is Pro (or use the key saved by `bdui login`)"
|
|
95
109
|
)
|
|
96
110
|
.action(cmdSkill);
|
|
97
111
|
|
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 { licenseFile, readSavedKey } from "./auth.js";
|
|
11
12
|
import { itemHints } from "./hints.js";
|
|
12
13
|
import { detectProject } from "./project.js";
|
|
13
14
|
import { fetchIndex, fetchItem } from "./registry.js";
|
|
@@ -38,7 +39,7 @@ const SCHEMAS = {
|
|
|
38
39
|
.string()
|
|
39
40
|
.optional()
|
|
40
41
|
.describe(
|
|
41
|
-
"License key for Pro items; optional when the server was started with --key
|
|
42
|
+
"License key for Pro items; optional when the server was started with --key or a key was saved via `bdui login`."
|
|
42
43
|
),
|
|
43
44
|
}),
|
|
44
45
|
get_usage_examples: z.object({
|
|
@@ -51,7 +52,7 @@ const SCHEMAS = {
|
|
|
51
52
|
.string()
|
|
52
53
|
.optional()
|
|
53
54
|
.describe(
|
|
54
|
-
"License key for Pro items (same as `add --key`); optional when the server was started with --key
|
|
55
|
+
"License key for Pro items (same as `add --key`); optional when the server was started with --key or a key was saved via `bdui login`."
|
|
55
56
|
),
|
|
56
57
|
overwrite: z.boolean().optional(),
|
|
57
58
|
installDependencies: z.boolean().optional(),
|
|
@@ -118,8 +119,8 @@ function groupLabel(t) {
|
|
|
118
119
|
);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
// serverKey:`bdui mcp --key <key>` 启动内嵌(.mcp.json args)
|
|
122
|
-
//
|
|
122
|
+
// serverKey:`bdui mcp --key <key>` 启动内嵌(.mcp.json args)或 `bdui login` 落盘,
|
|
123
|
+
// 整会话兜底;调用级 key 参数优先。
|
|
123
124
|
async function loadItem(name, key, serverKey) {
|
|
124
125
|
return fetchItem(name, key ?? serverKey);
|
|
125
126
|
}
|
|
@@ -137,10 +138,9 @@ async function dispatch(name, args, captured, serverKey) {
|
|
|
137
138
|
"3. get_usage_examples — working code for an item.",
|
|
138
139
|
"4. install_components — writes files, resolves internal deps recursively, installs npm deps.",
|
|
139
140
|
"",
|
|
140
|
-
"Pro items need
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"CLI equivalents: npx @collectui/bdui init | list | add <name> [--key <key>] | skill | mcp [--key <key>].",
|
|
141
|
+
"Pro items need a license key. Recommended: save it once with `npx -y @collectui/bdui login --key <key>` — stored at ~/.config/bdui/license.json (0600, this machine only), and every later CLI command and MCP session is unlocked without passing anything.",
|
|
142
|
+
"Alternatives (nothing stored): pass `key` per call to install_components (same as the CLI's `add --key <license-key>`), or start this server with `npx -y @collectui/bdui mcp --key <key>` (embed it in the args block of your MCP config).",
|
|
143
|
+
"CLI equivalents: npx @collectui/bdui init | list | add <name> [--key <key>] | skill | mcp [--key <key>] | login | logout.",
|
|
144
144
|
].join("\n");
|
|
145
145
|
|
|
146
146
|
case "list_components": {
|
|
@@ -369,12 +369,14 @@ async function call(name, args, serverKey) {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
export async function cmdMcp(options = {}) {
|
|
372
|
-
const serverKey = options.key ??
|
|
373
|
-
// stderr 提示(stdio 协议,stdout 必须干净)
|
|
374
|
-
if (
|
|
372
|
+
const serverKey = options.key ?? readSavedKey(); // `bdui mcp --key` 内嵌 or `bdui login` 落盘,见 loadItem 注释
|
|
373
|
+
// stderr 提示(stdio 协议,stdout 必须干净):显式 --key 常嵌 .mcp.json,防 commit 泄漏(盲测 #3)
|
|
374
|
+
if (options.key)
|
|
375
375
|
console.error(
|
|
376
376
|
"[bdui] tip: if this key is embedded in .mcp.json, keep that file out of git (add it to .gitignore)."
|
|
377
377
|
);
|
|
378
|
+
else if (serverKey)
|
|
379
|
+
console.error(`[bdui] using saved key from ${licenseFile()}`);
|
|
378
380
|
const rl = createInterface({ input: process.stdin });
|
|
379
381
|
const write = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
|
|
380
382
|
|
package/src/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Registry 拉取 — shadcn registry.json schema 兼容
|
|
2
2
|
// 公共: GET {BASE}/r/registry.json 与 /r/<name>.json (免鉴权)
|
|
3
|
-
// Pro: GET {BASE}/api/pro/r/<name> Bearer <key>(
|
|
4
|
-
// 端点内置,不提供 env
|
|
3
|
+
// Pro: GET {BASE}/api/pro/r/<name> Bearer <key>(resolveKey 三级解析:--key flag > BDUI_KEY env > `bdui login` 落盘;校验/缓存/熔断全在服务端)
|
|
4
|
+
// 端点内置,不提供 env 覆盖。
|
|
5
5
|
const BASE = "https://boardui.collectui.pro";
|
|
6
6
|
|
|
7
7
|
export const registryBaseUrl = () => `${BASE}/r`;
|
|
@@ -43,7 +43,7 @@ export async function fetchItem(name, key) {
|
|
|
43
43
|
if (error?.status !== 404 && error?.status !== 401) throw error;
|
|
44
44
|
if (!key) {
|
|
45
45
|
throw new Error(
|
|
46
|
-
`"${name}" is a BoardUI Pro component.
|
|
46
|
+
`"${name}" is a BoardUI Pro component. Run \`bdui login --key <license-key>\` once to save your key on this machine, or pass --key <license-key> to this command. The key is distributed by your platform or administrator.`
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
const response = await fetch(`${proRegistryBaseUrl()}/${name}`, {
|
package/src/skill.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `bdui skill` — 从 registry 拉 skill 条目(SKILL.md + references)写入目标目录
|
|
2
2
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { resolveKey } from "./auth.js";
|
|
4
5
|
import c from "./color.js";
|
|
5
6
|
import { fetchItem } from "./registry.js";
|
|
6
7
|
|
|
@@ -15,7 +16,7 @@ export async function cmdSkill(flags) {
|
|
|
15
16
|
target = path.join(home, ".agents", "skills", "boardui");
|
|
16
17
|
else target = path.join(process.cwd(), ".claude", "skills", "boardui");
|
|
17
18
|
|
|
18
|
-
const skill = await fetchItem("skill", flags.key);
|
|
19
|
+
const skill = await fetchItem("skill", resolveKey(flags.key));
|
|
19
20
|
if (existsSync(target) && !flags.force) {
|
|
20
21
|
throw new Error(
|
|
21
22
|
`${target} already exists — pass --force to overwrite (how you update).`
|
package/src/write.js
CHANGED
|
@@ -31,9 +31,15 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
|
|
|
31
31
|
let wroteStyles = false;
|
|
32
32
|
const state = { all: false };
|
|
33
33
|
for (const file of item.files ?? []) {
|
|
34
|
+
// 空元素/空路径守卫:官方条目实发过 assets:[null] 同型数据(2026-11-06
|
|
35
|
+
// sidebar 实测 EISDIR),空 path 会让 target 落在 outRoot/public 目录本身
|
|
36
|
+
if (!file?.path) {
|
|
37
|
+
console.log(` ${pc.yellow("skip")} file entry without a path`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
34
40
|
// 路径守卫:file.path 不得逃逸 outRoot(防 ../ 遍历;信任官方 registry
|
|
35
41
|
// 前提下是纵深防御,与 shadcn 同水位再进一步)
|
|
36
|
-
const rel = path.normalize(file.path
|
|
42
|
+
const rel = path.normalize(file.path);
|
|
37
43
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
38
44
|
console.log(` ${pc.red("skip")} unsafe path in item: ${file.path}`);
|
|
39
45
|
continue;
|
|
@@ -56,7 +62,13 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
|
|
|
56
62
|
}
|
|
57
63
|
// 二进制 assets(base64):Pro 模板的头像/logo 等图片资源(盲测 r2 P1)
|
|
58
64
|
for (const asset of item.assets ?? []) {
|
|
59
|
-
|
|
65
|
+
// 空元素/空路径守卫:官方 sidebar 条目实发 assets:[null]——null 直接读 path
|
|
66
|
+
// 抛 TypeError,空 path 则 target 落在 public 目录本身 writeFileSync 抛 EISDIR
|
|
67
|
+
if (!asset?.path) {
|
|
68
|
+
console.log(` ${pc.yellow("skip")} asset entry without a path`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const rel = path.normalize(asset.path);
|
|
60
72
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
61
73
|
console.log(` ${pc.red("skip")} unsafe path in asset: ${asset.path}`);
|
|
62
74
|
continue;
|