bfi-cli 0.1.99 → 0.1.101
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/README.md +117 -5
- package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
- package/dist/_chunks/agents.mjs +1 -1
- package/dist/_chunks/agents2.mjs +4 -1
- package/dist/_chunks/client.mjs +107 -0
- package/dist/_chunks/command.mjs +697 -0
- package/dist/_chunks/digest.mjs +132 -0
- package/dist/_chunks/files.mjs +89 -0
- package/dist/_chunks/{registry-client.mjs → intent.mjs} +163 -13
- package/dist/_chunks/libs/@clack/core.mjs +63 -58
- package/dist/_chunks/libs/@clack/prompts.mjs +46 -45
- package/dist/_chunks/libs/@kwsites/file-exists.mjs +10 -9
- package/dist/_chunks/libs/simple-git.mjs +4 -2
- package/dist/_chunks/manager.mjs +436 -0
- package/dist/_chunks/mcp.mjs +559 -0
- package/dist/_chunks/model.mjs +124 -0
- package/dist/_chunks/ownership.mjs +7 -0
- package/dist/_chunks/package-plugin.mjs +473 -36
- package/dist/_chunks/receipt.mjs +166 -0
- package/dist/_chunks/rolldown-runtime.mjs +1 -1
- package/dist/_chunks/skills.mjs +6 -3
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +254 -221
- package/dist/mcp-codesearch/main.d.mts +1 -1
- package/dist/mcp-db/main.d.mts +1 -1
- package/dist/mcp-weknora/main.d.mts +1 -1
- package/package.json +4 -2
- package/dist/THIRD-PARTY-LICENSES.md +0 -293
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { checkPath, readConfig } from "./files.mjs";
|
|
2
|
+
import { canonical, hash } from "./digest.mjs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { constants, realpathSync } from "node:fs";
|
|
5
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
6
|
+
import { access, lstat, mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
function pathKey(path) {
|
|
9
|
+
return resolve(path).normalize("NFC").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
function ownershipDigest(records) {
|
|
12
|
+
return hash(canonical([...records].sort((left, right) => left.id.localeCompare(right.id)).map((record) => ({
|
|
13
|
+
id: record.id,
|
|
14
|
+
state: record.state,
|
|
15
|
+
actions: record.actions
|
|
16
|
+
}))));
|
|
17
|
+
}
|
|
18
|
+
function stateRoot(home = homedir()) {
|
|
19
|
+
return join(realpathSync(home), ".bfi/installations");
|
|
20
|
+
}
|
|
21
|
+
function operationRoot(id, home) {
|
|
22
|
+
if (!/^[0-9a-f-]{36}$/.test(id)) throw new Error("无效的操作 ID。");
|
|
23
|
+
return join(stateRoot(home), id);
|
|
24
|
+
}
|
|
25
|
+
async function safeDirectory(path) {
|
|
26
|
+
let current = resolve(path);
|
|
27
|
+
while (current !== parse(current).root) {
|
|
28
|
+
try {
|
|
29
|
+
const stat = await lstat(current);
|
|
30
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("目录含符号链接或非目录,拒绝安装。");
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT") throw error;
|
|
33
|
+
}
|
|
34
|
+
current = dirname(current);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function assertWritable(path, directory = false) {
|
|
38
|
+
if (directory) await safeDirectory(path);
|
|
39
|
+
else await checkPath(path);
|
|
40
|
+
let current = path;
|
|
41
|
+
while (true) try {
|
|
42
|
+
if (!((await lstat(current)).mode & 128)) throw new Error("安装目标只读。");
|
|
43
|
+
await access(current, constants.W_OK);
|
|
44
|
+
break;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== "ENOENT") throw error;
|
|
47
|
+
current = dirname(current);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function atomicPrivate(path, data) {
|
|
51
|
+
await checkPath(path);
|
|
52
|
+
await mkdir(dirname(path), {
|
|
53
|
+
recursive: true,
|
|
54
|
+
mode: 448
|
|
55
|
+
});
|
|
56
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
57
|
+
try {
|
|
58
|
+
const handle = await open(temporary, "wx", 384);
|
|
59
|
+
try {
|
|
60
|
+
await handle.writeFile(data);
|
|
61
|
+
await handle.sync();
|
|
62
|
+
} finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
await rename(temporary, path);
|
|
66
|
+
const parent = await open(dirname(path), "r");
|
|
67
|
+
try {
|
|
68
|
+
await parent.sync();
|
|
69
|
+
} finally {
|
|
70
|
+
await parent.close();
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
await rm(temporary, { force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function checkpoint(receipt, home) {
|
|
77
|
+
await atomicPrivate(join(operationRoot(receipt.id, home), "receipt.json"), JSON.stringify(receipt));
|
|
78
|
+
}
|
|
79
|
+
async function readReceipt(id, home) {
|
|
80
|
+
const path = join(operationRoot(id, home), "receipt.json");
|
|
81
|
+
await checkPath(path);
|
|
82
|
+
const stat = await lstat(path);
|
|
83
|
+
if (stat.size > 32 * 1024 ** 2 || stat.mode & 63) throw new Error("安装回执大小或权限不安全。");
|
|
84
|
+
const receipt = JSON.parse(await readFile(path, "utf8"));
|
|
85
|
+
if (receipt.id !== id || !Array.isArray(receipt.actions)) throw new Error("安装回执无效。");
|
|
86
|
+
return receipt;
|
|
87
|
+
}
|
|
88
|
+
async function receipts(home) {
|
|
89
|
+
await safeDirectory(stateRoot(home));
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = await readdir(stateRoot(home));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code === "ENOENT") return [];
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
const result = [];
|
|
98
|
+
for (const entry of entries.filter((name) => /^[0-9a-f-]{36}$/.test(name))) try {
|
|
99
|
+
result.push(await readReceipt(entry, home));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code !== "ENOENT") throw error;
|
|
102
|
+
}
|
|
103
|
+
const replaced = new Set(result.filter((record) => record.state === "INSTALLED" || record.state === "REMOVED").map((record) => record.replaces));
|
|
104
|
+
return result.filter((record) => !replaced.has(record.id) && record.state !== "REMOVED");
|
|
105
|
+
}
|
|
106
|
+
async function fingerprint(path, kind) {
|
|
107
|
+
if (kind === "mcp") {
|
|
108
|
+
const text = await readConfig(path);
|
|
109
|
+
return text === void 0 ? void 0 : hash(text);
|
|
110
|
+
}
|
|
111
|
+
await safeDirectory(path);
|
|
112
|
+
try {
|
|
113
|
+
await lstat(path);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code === "ENOENT") return;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
const files = [];
|
|
119
|
+
let total = 0;
|
|
120
|
+
const walk = async (folder, prefix) => {
|
|
121
|
+
for (const entry of await readdir(folder, { withFileTypes: true })) {
|
|
122
|
+
const filename = join(folder, entry.name);
|
|
123
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
124
|
+
const stat = await lstat(filename);
|
|
125
|
+
if (stat.isSymbolicLink() || stat.nlink > 1 && stat.isFile()) throw new Error("技能含链接,拒绝覆盖。");
|
|
126
|
+
if (stat.isDirectory()) await walk(filename, relative);
|
|
127
|
+
else if (stat.isFile()) {
|
|
128
|
+
total += stat.size;
|
|
129
|
+
if (total > 500 * 1024 ** 2 || stat.size > 50 * 1024 ** 2 || files.length >= 2e4) throw new Error("本地技能超过安全上限。");
|
|
130
|
+
files.push({
|
|
131
|
+
path: relative,
|
|
132
|
+
sha256: hash(await readFile(filename)),
|
|
133
|
+
executable: Boolean(stat.mode & 73)
|
|
134
|
+
});
|
|
135
|
+
} else throw new Error("技能含特殊文件。");
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
await walk(path, "");
|
|
139
|
+
return treeFingerprint(files);
|
|
140
|
+
}
|
|
141
|
+
function treeFingerprint(files) {
|
|
142
|
+
return hash(canonical([...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)));
|
|
143
|
+
}
|
|
144
|
+
async function withInstallationLock(work, home) {
|
|
145
|
+
const root = stateRoot(home);
|
|
146
|
+
await safeDirectory(root);
|
|
147
|
+
await mkdir(root, {
|
|
148
|
+
recursive: true,
|
|
149
|
+
mode: 448
|
|
150
|
+
});
|
|
151
|
+
const filename = join(root, "active.lock");
|
|
152
|
+
let handle;
|
|
153
|
+
try {
|
|
154
|
+
handle = await open(filename, "wx", 384);
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error("已有安装或残留执行锁:请确认没有 bfi 安装进程后移除 ~/.bfi/installations/active.lock,再用 --operation 恢复。");
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await handle.writeFile(String(process.pid));
|
|
160
|
+
return await work();
|
|
161
|
+
} finally {
|
|
162
|
+
await handle.close();
|
|
163
|
+
await rm(filename, { force: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export { assertWritable, atomicPrivate, checkpoint, fingerprint, operationRoot, ownershipDigest, pathKey, readReceipt, receipts, safeDirectory, treeFingerprint, withInstallationLock };
|
|
@@ -25,7 +25,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
25
25
|
}
|
|
26
26
|
return to;
|
|
27
27
|
};
|
|
28
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
29
29
|
value: mod,
|
|
30
30
|
enumerable: true
|
|
31
31
|
}) : target, mod));
|
package/dist/_chunks/skills.mjs
CHANGED
|
@@ -127,7 +127,8 @@ async function readSkillLock() {
|
|
|
127
127
|
async function writeSkillLock(lock) {
|
|
128
128
|
const lockPath = getSkillLockPath();
|
|
129
129
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
130
|
-
|
|
130
|
+
const content = JSON.stringify(lock, null, 2);
|
|
131
|
+
await writeFile(lockPath, content, "utf-8");
|
|
131
132
|
}
|
|
132
133
|
let _ghWarningShown = false;
|
|
133
134
|
function getGitHubToken() {
|
|
@@ -220,7 +221,8 @@ async function writeLocalLock(lock, cwd) {
|
|
|
220
221
|
version: lock.version,
|
|
221
222
|
skills: sortedSkills
|
|
222
223
|
};
|
|
223
|
-
|
|
224
|
+
const content = JSON.stringify(sorted, null, 2) + "\n";
|
|
225
|
+
await writeFile(lockPath, content, "utf-8");
|
|
224
226
|
}
|
|
225
227
|
async function computeSkillFolderHash(skillDir) {
|
|
226
228
|
const files = [];
|
|
@@ -308,7 +310,8 @@ function shouldInstallInternalSkills() {
|
|
|
308
310
|
}
|
|
309
311
|
async function hasSkillMd(dir) {
|
|
310
312
|
try {
|
|
311
|
-
|
|
313
|
+
const skillPath = join(dir, "SKILL.md");
|
|
314
|
+
return (await stat(skillPath)).isFile();
|
|
312
315
|
} catch {
|
|
313
316
|
return false;
|
|
314
317
|
}
|
package/dist/cli.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|