bfi-cli 0.1.98 → 0.1.100

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.
@@ -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));
@@ -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
- await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
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
- await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
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
- return (await stat(join(dir, "SKILL.md"))).isFile();
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 {}