agent-purge 0.2.10 → 0.2.12
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/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -21
- package/README.md +169 -138
- package/backends/index.mjs +204 -244
- package/bench/bench-armor.mjs +844 -832
- package/bench/capture-hot.mjs +58 -0
- package/bench/capture-provider.mjs +66 -0
- package/bench/capture-report.mjs +15 -0
- package/bench/capture-server-hot.mjs +55 -0
- package/bench/capture-system.mjs +36 -0
- package/bench/judge-armor.mjs +119 -119
- package/bin/agent-purge.mjs +956 -780
- package/docs/LEGAL.md +27 -0
- package/docs/architecture.md +39 -0
- package/docs/compatibility.md +78 -0
- package/docs/images/console-desktop.png +0 -0
- package/docs/images/console-mobile.png +0 -0
- package/docs/releasing.md +87 -0
- package/docs/usage.md +158 -0
- package/lib/activation.mjs +38 -0
- package/lib/apply.mjs +227 -0
- package/lib/armed.mjs +38 -38
- package/lib/armor.mjs +225 -222
- package/lib/caps.mjs +182 -173
- package/lib/capture.mjs +324 -0
- package/lib/clean.mjs +124 -0
- package/lib/control.mjs +60 -0
- package/lib/detect.mjs +74 -70
- package/lib/doctor.mjs +7 -7
- package/lib/dsh.mjs +495 -453
- package/lib/environment.mjs +36 -0
- package/lib/evidence.mjs +41 -0
- package/lib/extension.mjs +140 -0
- package/lib/footprints.mjs +59 -0
- package/lib/home.mjs +47 -47
- package/lib/launch-conflicts.mjs +24 -0
- package/lib/launch.mjs +57 -0
- package/lib/opencode.mjs +62 -0
- package/lib/paths.mjs +25 -0
- package/lib/persist.mjs +439 -280
- package/lib/persona-import.mjs +351 -0
- package/lib/personas.mjs +17 -7
- package/lib/private-server.mjs +54 -0
- package/lib/project.mjs +134 -0
- package/lib/restore.mjs +131 -114
- package/lib/rewrite.mjs +124 -0
- package/lib/root.mjs +37 -0
- package/lib/shim.mjs +203 -160
- package/lib/slashes.mjs +227 -628
- package/lib/spawn.mjs +182 -126
- package/lib/state.mjs +85 -137
- package/lib/templates/personas/neutral.md +30 -30
- package/lib/update.mjs +137 -0
- package/lib/verification.mjs +23 -0
- package/lib/window.mjs +58 -0
- package/lib/wrappers.mjs +49 -0
- package/magpie-plugin/README.md +44 -0
- package/matrix.json +325 -341
- package/package.json +30 -4
- package/web/app.js +590 -0
- package/web/assets/agent-purge.ico +0 -0
- package/web/assets/claude.ico +0 -0
- package/web/assets/codebuddy.png +0 -0
- package/web/assets/codex.webp +0 -0
- package/web/assets/dsh.ico +0 -0
- package/web/assets/grok.ico +0 -0
- package/web/assets/hermes.ico +0 -0
- package/web/assets/omp.png +0 -0
- package/web/assets/opencode.ico +0 -0
- package/web/assets/opencode2.ico +0 -0
- package/web/assets/pi.png +0 -0
- package/web/assets/zcode.png +0 -0
- package/web/model.mjs +49 -0
- package/web/server.mjs +330 -191
- package/web/styles.css +1072 -0
- package/web/ui.html +76 -1163
package/lib/restore.mjs
CHANGED
|
@@ -1,114 +1,131 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export
|
|
1
|
+
// 临时文件交换:独占 journal + 所有者存活检查 + 写后指纹。
|
|
2
|
+
// 冲突或恢复失败保留 journal,不覆盖用户在测试期间的修改。
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
const JOURNAL_DIR = process.env.AGENT_PURGE_JOURNAL_DIR || path.join(os.tmpdir(), "agent-purge-restore");
|
|
9
|
+
let installed = false;
|
|
10
|
+
const live = new Set();
|
|
11
|
+
const hash = (bytes) => bytes === null ? null : createHash("sha256").update(bytes).digest("hex");
|
|
12
|
+
const read = (file) => { try { return fs.readFileSync(file); } catch (e) { if (e.code === "ENOENT") return null; throw e; } };
|
|
13
|
+
function canonical(file) {
|
|
14
|
+
const full = path.resolve(file);
|
|
15
|
+
// 同一目录的符号链接也应指向同一把锁;不跟随不存在的目标。
|
|
16
|
+
try { return fs.realpathSync(full); } catch (e) {
|
|
17
|
+
if (e.code !== "ENOENT") throw e;
|
|
18
|
+
return path.join(fs.realpathSync(path.dirname(full)), path.basename(full));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function entryPath(file) {
|
|
22
|
+
const key = process.platform === "win32" ? file.toLowerCase() : file;
|
|
23
|
+
return path.join(JOURNAL_DIR, createHash("sha256").update(key).digest("hex") + ".json");
|
|
24
|
+
}
|
|
25
|
+
function alive(pid) {
|
|
26
|
+
if (!Number.isInteger(pid) || pid <= 0) return true;
|
|
27
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code !== "ESRCH"; }
|
|
28
|
+
}
|
|
29
|
+
function atomicWrite(file, bytes, mode) {
|
|
30
|
+
const tmp = file + ".purge-" + randomUUID() + ".tmp";
|
|
31
|
+
try {
|
|
32
|
+
fs.writeFileSync(tmp, bytes, { flag: "wx", mode: mode ?? 0o600 });
|
|
33
|
+
fs.renameSync(tmp, file);
|
|
34
|
+
} finally { fs.rmSync(tmp, { force: true }); }
|
|
35
|
+
}
|
|
36
|
+
function restore(record, journal) {
|
|
37
|
+
let currentRecord;
|
|
38
|
+
try {
|
|
39
|
+
const source = fs.readFileSync(journal, "utf8");
|
|
40
|
+
currentRecord = JSON.parse(source);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`无法读取恢复凭据,文件未改动:${journal}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
if (currentRecord.operation !== record.operation) throw new Error(`恢复所有权已改变:${record.file}`);
|
|
45
|
+
const current = hash(read(record.file));
|
|
46
|
+
if (current !== record.beforeHash && current !== record.afterHash) {
|
|
47
|
+
throw new Error(`文件在交换后被修改,保留 journal 等待处理:${record.file}`);
|
|
48
|
+
}
|
|
49
|
+
if (current !== record.beforeHash) {
|
|
50
|
+
if (record.backup === null) fs.rmSync(record.file, { force: true });
|
|
51
|
+
else atomicWrite(record.file, Buffer.from(record.backup, "base64"), record.mode);
|
|
52
|
+
}
|
|
53
|
+
fs.rmSync(journal);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 独占交换;返回可重试、幂等的 undo。恢复失败会抛错且保留原始备份。 */
|
|
57
|
+
export function swapFile(file, content) {
|
|
58
|
+
file = canonical(file);
|
|
59
|
+
fs.mkdirSync(JOURNAL_DIR, { recursive: true });
|
|
60
|
+
const journal = entryPath(file);
|
|
61
|
+
let fd;
|
|
62
|
+
try { fd = fs.openSync(journal, "wx", 0o600); }
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e.code === "EEXIST") throw new Error(`文件存在未结束的交换,请先处理 journal:${file}`);
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
let record;
|
|
68
|
+
try {
|
|
69
|
+
const backup = read(file);
|
|
70
|
+
const next = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
71
|
+
record = { version: 2, file, pid: process.pid, operation: randomUUID(), at: new Date().toISOString(),
|
|
72
|
+
backup: backup === null ? null : backup.toString("base64"), beforeHash: hash(backup), afterHash: hash(next),
|
|
73
|
+
mode: backup === null ? undefined : fs.statSync(file).mode & 0o777 };
|
|
74
|
+
fs.writeFileSync(fd, JSON.stringify(record));
|
|
75
|
+
fs.fsyncSync(fd);
|
|
76
|
+
fs.closeSync(fd); fd = undefined;
|
|
77
|
+
atomicWrite(file, next, record.mode);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
80
|
+
if (record) { try { restore(record, journal); } catch {} }
|
|
81
|
+
else fs.rmSync(journal, { force: true });
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
let done = false;
|
|
85
|
+
const undo = () => {
|
|
86
|
+
if (done) return;
|
|
87
|
+
restore(record, journal);
|
|
88
|
+
done = true;
|
|
89
|
+
live.delete(undo);
|
|
90
|
+
};
|
|
91
|
+
live.add(undo);
|
|
92
|
+
return undo;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 只恢复已死亡的 v2 所有者;旧格式无法证明所有权,保留并报告。 */
|
|
96
|
+
export function recover({ onWarning = (message) => console.error(`[restore] ${message}`) } = {}) {
|
|
97
|
+
fs.mkdirSync(JOURNAL_DIR, { recursive: true });
|
|
98
|
+
let n = 0;
|
|
99
|
+
for (const name of fs.readdirSync(JOURNAL_DIR)) {
|
|
100
|
+
if (!name.endsWith(".json")) continue;
|
|
101
|
+
const journal = path.join(JOURNAL_DIR, name);
|
|
102
|
+
try {
|
|
103
|
+
const record = JSON.parse(fs.readFileSync(journal, "utf8"));
|
|
104
|
+
if (record.version !== 2 || !record.operation || !path.isAbsolute(record.file || "")) {
|
|
105
|
+
onWarning(`旧版或不完整 journal,未自动覆盖文件:${journal}`); continue;
|
|
106
|
+
}
|
|
107
|
+
if (alive(record.pid)) continue;
|
|
108
|
+
restore(record, journal); n++;
|
|
109
|
+
} catch (e) { onWarning(`${journal}: ${e.message}`); }
|
|
110
|
+
}
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function installCrashHandlers() {
|
|
115
|
+
if (installed) return;
|
|
116
|
+
installed = true;
|
|
117
|
+
const flush = () => {
|
|
118
|
+
for (const undo of [...live]) {
|
|
119
|
+
try { undo(); } catch (e) { console.error(`[restore] ${e.message}`); }
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"]) {
|
|
123
|
+
try { process.on(sig, () => { flush(); process.exit(sig === "SIGINT" ? 130 : 143); }); } catch {}
|
|
124
|
+
}
|
|
125
|
+
process.on("uncaughtException", (e) => {
|
|
126
|
+
console.error("[restore] 未捕获异常:", e?.stack ?? e);
|
|
127
|
+
flush(); process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
process.on("exit", flush);
|
|
130
|
+
}
|
|
131
|
+
export const journalDir = JOURNAL_DIR;
|
package/lib/rewrite.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// 模式语义与生成插件共享实现;运行时只还原有来源记录的完整变换。
|
|
2
|
+
import { activationRuntime } from "./activation.mjs";
|
|
3
|
+
export function stripIdentity(text) {
|
|
4
|
+
const self = /^\s*(?:You are|You're|你是|您是|我(?:是|现在扮演|就是))/i;
|
|
5
|
+
const named = /(Claude|ChatGPT|GPT-?[45]|Gemini|Codex|Copilot|DeepSeek|Qwen|Kimi|GLM|Cursor|Windsurf|OpenCode|ZCode|CodeBuddy|Hermes|Grok|Antigravity|AI (?:assistant|model)|coding (?:assistant|agent)|语言模型|助手)/i;
|
|
6
|
+
return String(text ?? "").split(/\r?\n/).filter(line => !(self.test(line) && named.test(line))).join("\n").trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function cleanManaged(text, bodies = []) {
|
|
10
|
+
let out = String(text ?? "").replace(/^# >>> agent-purge[^\S\r\n]*\r?\n[\s\S]*?^# <<< agent-purge[^\S\r\n]*(?:\r?\n|$)/gm, "");
|
|
11
|
+
for (const body of bodies) {
|
|
12
|
+
if (!body) continue;
|
|
13
|
+
// 只删除独立完整正文;共享前40字符的另一份人设/用户规则不匹配。
|
|
14
|
+
const literal = String(body).trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
|
+
out = out.replace(new RegExp("(^|\\n)" + literal + "(?=\\n|$)", "g"), "$1");
|
|
16
|
+
}
|
|
17
|
+
return out.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rewriteTexts(hostTexts, persona, mode) {
|
|
21
|
+
if (mode === "off" || !persona) return hostTexts;
|
|
22
|
+
const host = (Array.isArray(hostTexts) ? hostTexts : []).map(t => String(t ?? "")).filter(t => t.trim());
|
|
23
|
+
if (mode === "strict") return [persona];
|
|
24
|
+
if (mode === "append") return [...host, persona];
|
|
25
|
+
return [persona, ...host.map(stripIdentity).filter(Boolean)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function rewriteSections(sections, persona, mode, name = "agent-purge") {
|
|
29
|
+
const list = Array.isArray(sections) ? sections : [];
|
|
30
|
+
if (mode === "off" || !persona) return list;
|
|
31
|
+
if (mode === "strict") return [{ name, text: persona, interpolate: false }];
|
|
32
|
+
if (mode === "append") return [...list, { name, text: persona, interpolate: false }];
|
|
33
|
+
const kept = list.map(s => typeof s?.text === "string" ? { ...s, text: stripIdentity(s.text) } : s)
|
|
34
|
+
.filter(s => typeof s?.text !== "string" || s.text.trim());
|
|
35
|
+
return [{ name, text: persona, interpolate: false }, ...kept];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 有界完整变换记录:无凭据或歧义时原样保留,不按标签/人设文本猜测删除。 */
|
|
39
|
+
export function createProvenance(limit = 64) {
|
|
40
|
+
const values = new Map();
|
|
41
|
+
const objects = new WeakMap();
|
|
42
|
+
const key = value => JSON.stringify(value);
|
|
43
|
+
function remember(before, after, aliases = []) {
|
|
44
|
+
const beforeKey = key(before), afterKey = key(after);
|
|
45
|
+
if (beforeKey === afterKey) return after;
|
|
46
|
+
const entry = { before: structuredClone(before), beforeKey, afterKey };
|
|
47
|
+
if (after && typeof after === "object") objects.set(after, entry);
|
|
48
|
+
for (const candidate of [after, ...aliases]) {
|
|
49
|
+
const id = key(candidate), previous = values.get(id);
|
|
50
|
+
values.set(id, previous && previous.beforeKey !== beforeKey ? { ambiguous: true } : previous?.ambiguous ? previous : entry);
|
|
51
|
+
}
|
|
52
|
+
while (values.size > limit) values.delete(values.keys().next().value);
|
|
53
|
+
return after;
|
|
54
|
+
}
|
|
55
|
+
function restore(value) {
|
|
56
|
+
const id = key(value);
|
|
57
|
+
const direct = value && typeof value === "object" ? objects.get(value) : null;
|
|
58
|
+
const entry = direct?.afterKey === id ? direct : values.get(id);
|
|
59
|
+
return entry && !entry.ambiguous ? structuredClone(entry.before) : value;
|
|
60
|
+
}
|
|
61
|
+
return { remember, restore };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createTextRewriter() {
|
|
65
|
+
const provenance = createProvenance();
|
|
66
|
+
return (texts, persona, mode) => {
|
|
67
|
+
const host = provenance.restore(texts);
|
|
68
|
+
if (mode === "off" || !persona) return host;
|
|
69
|
+
const next = rewriteTexts(host, persona, mode);
|
|
70
|
+
return provenance.remember(host, next, [[next.join("\n\n")]]);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createSectionRewriter() {
|
|
75
|
+
const provenance = createProvenance();
|
|
76
|
+
return (sections, persona, mode, name = "agent-purge") => {
|
|
77
|
+
const host = provenance.restore(sections);
|
|
78
|
+
if (mode === "off" || !persona) return host;
|
|
79
|
+
return provenance.remember(host, rewriteSections(host, persona, mode, name));
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Anthropic-style content blocks: retain non-text blocks and cache metadata. */
|
|
84
|
+
export function createContentRewriter() {
|
|
85
|
+
const provenance = createProvenance();
|
|
86
|
+
return (blocks, persona, mode) => {
|
|
87
|
+
const host = provenance.restore(blocks);
|
|
88
|
+
if (mode === "off" || !persona || !Array.isArray(host)) return host;
|
|
89
|
+
let next;
|
|
90
|
+
if (mode === "strict") {
|
|
91
|
+
const text = host.find(b => b?.type === "text");
|
|
92
|
+
const own = { ...text, type: "text", text: persona };
|
|
93
|
+
next = [own, ...host.filter(b => b?.type !== "text")];
|
|
94
|
+
} else {
|
|
95
|
+
const kept = mode === "replace" ? host.map(b => typeof b?.text === "string" ? { ...b, text: stripIdentity(b.text) } : b) : host;
|
|
96
|
+
const own = { type: "text", text: persona };
|
|
97
|
+
next = mode === "append" ? [...kept, own] : [own, ...kept];
|
|
98
|
+
}
|
|
99
|
+
return provenance.remember(host, next);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const rewriteRuntime = [stripIdentity, cleanManaged, rewriteTexts, rewriteSections, createProvenance, createTextRewriter, createSectionRewriter, createContentRewriter]
|
|
104
|
+
.map(fn => fn.toString()).join("\n") + `
|
|
105
|
+
const apStrip = stripIdentity;
|
|
106
|
+
const apRewrite = createTextRewriter();
|
|
107
|
+
const apRewriteSections = createSectionRewriter();
|
|
108
|
+
const apRewriteContent = createContentRewriter();
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
export const resolveRuntime = activationRuntime;
|
|
112
|
+
|
|
113
|
+
/** 覆盖指定的人设直接读 personas/<id>.md(剥掉 frontmatter);读不到再回退嵌入的 active.md。 */
|
|
114
|
+
export const personaBodyRuntime = `
|
|
115
|
+
function resolvePersonaBody(personas, id, fallback) {
|
|
116
|
+
if (id && id !== "active" && /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(id)) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = readFileSync(personas + "/" + id + ".md", "utf8").replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---(?:\\r?\\n|$)/, "");
|
|
119
|
+
if (raw.trim()) return raw.trim();
|
|
120
|
+
} catch { return ""; }
|
|
121
|
+
}
|
|
122
|
+
return readFileSync(fallback, "utf8").trim();
|
|
123
|
+
}
|
|
124
|
+
`;
|
package/lib/root.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// 随包资源访问:源码态读磁盘,bun --compile 产物读内嵌表。
|
|
2
|
+
//
|
|
3
|
+
// 为什么不是「算一个 ROOT 再 join」:实测(bun 1.3.14, Windows)
|
|
4
|
+
// - `[dir]/[name].[ext]` 命名下根目录文件变成 `B:/~BUN/root/./matrix.json`,readFileSync 直接 ENOENT;
|
|
5
|
+
// - 拼接出来的 `B:/~BUN/root/matrix.json` 同样打不开;
|
|
6
|
+
// - bunfs 目录不支持 readdir。
|
|
7
|
+
// 只有「原样用 import 拿到的路径」能读,所以编译态的每条路径都由 build 入口生成的表给出,
|
|
8
|
+
// 键 = 仓库相对路径(posix 分隔)。源码态保持原来的 join(包根, 相对路径) 行为。
|
|
9
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const FILES = globalThis.__AGENT_PURGE_FILES ?? null;
|
|
14
|
+
|
|
15
|
+
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
|
|
17
|
+
/** 编译态:资源在 bunfs 里只读,任何「往包目录写」的逻辑都要绕开 */
|
|
18
|
+
export const isCompiled = () => FILES !== null;
|
|
19
|
+
|
|
20
|
+
/** 读一个随包资源(仓库相对路径,posix 分隔):matrix.json / web/ui.html / lib/templates/... */
|
|
21
|
+
export function readResource(rel) {
|
|
22
|
+
const hit = FILES?.[rel];
|
|
23
|
+
if (hit) return readFileSync(hit);
|
|
24
|
+
if (FILES) throw new Error(`exe 里没内嵌 ${rel}(scripts/build-exe.mjs 的 assets() 漏了它)`);
|
|
25
|
+
return readFileSync(join(ROOT, rel));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 列一个资源目录下的文件名(不含子目录):编译态筛内嵌表,源码态真读目录 */
|
|
29
|
+
export function listResources(prefix) {
|
|
30
|
+
const dir = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
31
|
+
if (FILES) {
|
|
32
|
+
return Object.keys(FILES)
|
|
33
|
+
.filter((k) => k.startsWith(dir) && !k.slice(dir.length).includes("/"))
|
|
34
|
+
.map((k) => k.slice(dir.length));
|
|
35
|
+
}
|
|
36
|
+
return readdirSync(join(ROOT, dir));
|
|
37
|
+
}
|