@join-pantheon/cli 0.29.0 → 0.30.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/README.md +45 -22
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +197 -14
- package/dist/claude-confine.js +283 -0
- package/dist/cli.js +725 -119
- package/dist/client.js +37 -2
- package/dist/command-state.js +79 -0
- package/dist/config.js +211 -10
- package/dist/doctor.js +103 -13
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +210 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +24 -0
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +507 -38
- package/dist/mcp-supervisor.js +25 -6
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +25 -10
- package/dist/network.js +39 -8
- package/dist/notify.js +17 -6
- package/dist/pending.js +8 -14
- package/dist/persona.js +21 -0
- package/dist/profile.js +4 -1
- package/dist/quickstart.js +88 -15
- package/dist/reads.js +30 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1193 -185
- package/dist/roster-file.js +169 -23
- package/dist/run-failure.js +83 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +105 -36
- package/dist/setup-guide.js +28 -9
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +11 -2
- package/dist/update-check.js +2 -0
- package/dist/which.js +69 -12
- package/dist/worker-policy.js +137 -3
- package/dist/worker.js +702 -81
- package/helper/macos/menubar.swift +15 -1
- package/package.json +2 -2
package/dist/install.js
CHANGED
|
@@ -10,6 +10,7 @@ import { loadProfile } from "./profile.js";
|
|
|
10
10
|
import { agentSpawn, onPathNamed, which } from "./which.js";
|
|
11
11
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
12
12
|
import { kiroDir, kiroAgentFile, refreshKiroAgentAllowed } from "./kiro-files.js";
|
|
13
|
+
import { eolOf, isJsonc, stripBom, stripJsonc, withEol } from "./text-file.js";
|
|
13
14
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
export function onPath() {
|
|
15
16
|
return onPathNamed("pantheon");
|
|
@@ -23,42 +24,65 @@ export function launcher(force, os = platform()) {
|
|
|
23
24
|
: { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
24
25
|
}
|
|
25
26
|
function readConfigJson(file) {
|
|
27
|
+
return readConfigDoc(file).doc;
|
|
28
|
+
}
|
|
29
|
+
function readConfigDoc(file) {
|
|
26
30
|
if (!existsSync(file))
|
|
27
|
-
return {};
|
|
31
|
+
return { doc: {}, text: null, jsonc: false };
|
|
28
32
|
const text = readFileSync(file, "utf8");
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const body = stripBom(text);
|
|
34
|
+
if (!body.trim())
|
|
35
|
+
return { doc: {}, text, jsonc: false };
|
|
36
|
+
const asObject = (d) => (d && typeof d === "object" && !Array.isArray(d) ? d : null);
|
|
31
37
|
try {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return doc;
|
|
38
|
+
const d = asObject(JSON.parse(body));
|
|
39
|
+
if (d)
|
|
40
|
+
return { doc: d, text, jsonc: false };
|
|
35
41
|
}
|
|
36
42
|
catch { }
|
|
43
|
+
if (isJsonc(body)) {
|
|
44
|
+
try {
|
|
45
|
+
const d = asObject(JSON.parse(stripJsonc(body)));
|
|
46
|
+
if (d)
|
|
47
|
+
return { doc: d, text, jsonc: true };
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
37
51
|
throw new Error(`${file} is not valid JSON — fix or move it, then re-run`);
|
|
38
52
|
}
|
|
53
|
+
function writeConfigJson(file, doc, was) {
|
|
54
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
55
|
+
const next = JSON.stringify(doc, null, 2) + "\n";
|
|
56
|
+
writeFileSync(file, was === null ? next : (was.charCodeAt(0) === 0xfeff ? "\uFEFF" : "") + withEol(next, eolOf(was)));
|
|
57
|
+
}
|
|
39
58
|
function mergeMcpJson(file, entry, shape = {}) {
|
|
40
|
-
const
|
|
59
|
+
const read = readConfigDoc(file);
|
|
60
|
+
const doc = read.doc;
|
|
41
61
|
const existed = existsSync(file);
|
|
42
62
|
doc.mcpServers ??= {};
|
|
43
63
|
const before = JSON.stringify(doc.mcpServers["pantheon"] ?? null);
|
|
44
64
|
doc.mcpServers["pantheon"] = { ...shape, ...entry, env: shape.env ?? {} };
|
|
45
65
|
if (before === JSON.stringify(doc.mcpServers["pantheon"]))
|
|
46
66
|
return "unchanged";
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
if (read.jsonc)
|
|
68
|
+
return `left as it is: ${jsoncNote(file, "mcpServers", doc.mcpServers["pantheon"])}`;
|
|
69
|
+
writeConfigJson(file, doc, read.text);
|
|
49
70
|
return existed ? "updated" : "created";
|
|
50
71
|
}
|
|
72
|
+
const jsoncNote = (file, key, entry) => `${file} has comments or trailing commas (JSONC), so it was left exactly as it is rather than rewritten without them — add this under "${key}" yourself: "pantheon": ${JSON.stringify(entry)}`;
|
|
51
73
|
export const COPILOT_ENTRY_SHAPE = { type: "local", tools: ["*"] };
|
|
52
74
|
export function mergeVsCodeMcpJson(file, entry) {
|
|
53
|
-
const
|
|
75
|
+
const read = readConfigDoc(file);
|
|
76
|
+
const doc = read.doc;
|
|
54
77
|
const existed = existsSync(file);
|
|
55
78
|
doc.servers ??= {};
|
|
56
79
|
const before = JSON.stringify(doc.servers["pantheon"] ?? null);
|
|
57
80
|
doc.servers["pantheon"] = { type: "stdio", command: entry.command, args: entry.args };
|
|
58
81
|
if (before === JSON.stringify(doc.servers["pantheon"]))
|
|
59
82
|
return "unchanged";
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
if (read.jsonc)
|
|
84
|
+
return `left as it is: ${jsoncNote(file, "servers", doc.servers["pantheon"])}`;
|
|
85
|
+
writeConfigJson(file, doc, read.text);
|
|
62
86
|
return existed ? "updated" : "created";
|
|
63
87
|
}
|
|
64
88
|
export const copilotMcpConfigFile = (home = homedir(), env = process.env) => join(env.COPILOT_HOME || join(home, ".copilot"), "mcp-config.json");
|
|
@@ -112,14 +136,16 @@ function upsertBrief(file, cfg) {
|
|
|
112
136
|
return "created";
|
|
113
137
|
}
|
|
114
138
|
const cur = readFileSync(file, "utf8");
|
|
139
|
+
const eol = eolOf(cur);
|
|
140
|
+
const ours = withEol(block, eol);
|
|
115
141
|
if (cur.includes(BRIEF_MARK)) {
|
|
116
|
-
const next = cur.replace(/<!-- pantheon:begin -->[\s\S]*?<!-- pantheon:end -->/,
|
|
142
|
+
const next = cur.replace(/<!-- pantheon:begin -->[\s\S]*?<!-- pantheon:end -->/, () => ours);
|
|
117
143
|
if (next === cur)
|
|
118
144
|
return "unchanged";
|
|
119
145
|
writeFileSync(file, next);
|
|
120
146
|
return "updated";
|
|
121
147
|
}
|
|
122
|
-
writeFileSync(file, cur.trimEnd() +
|
|
148
|
+
writeFileSync(file, cur.trimEnd() + eol + eol + ours + eol);
|
|
123
149
|
return "updated";
|
|
124
150
|
}
|
|
125
151
|
export function install(projectDir, cfg, force, os = platform(), claudeLocal = claudeLocalScope) {
|
|
@@ -196,8 +222,25 @@ function userEntry(os = platform()) {
|
|
|
196
222
|
const l = launcher();
|
|
197
223
|
return { command: l.command, args: l.args };
|
|
198
224
|
}
|
|
199
|
-
function isVersionManagedPath(p) {
|
|
200
|
-
|
|
225
|
+
export function isVersionManagedPath(p) {
|
|
226
|
+
const s = "[\\/\\\\]";
|
|
227
|
+
return new RegExp([
|
|
228
|
+
`${s}\\.nvm${s}versions${s}node${s}`,
|
|
229
|
+
`${s}\\.fnm${s}`,
|
|
230
|
+
`${s}fnm${s}node-versions${s}`,
|
|
231
|
+
`${s}\\.local${s}share${s}fnm${s}`,
|
|
232
|
+
`${s}fnm_multishells${s}`,
|
|
233
|
+
`${s}(?:mise|rtx)${s}installs${s}node(?:js)?${s}`,
|
|
234
|
+
`${s}\\.asdf${s}installs${s}nodejs${s}`,
|
|
235
|
+
`${s}n${s}versions${s}node${s}`,
|
|
236
|
+
`${s}\\.volta${s}tools${s}`,
|
|
237
|
+
].join("|")).test(p);
|
|
238
|
+
}
|
|
239
|
+
export function claudeAddFailed(e, command) {
|
|
240
|
+
const err = e;
|
|
241
|
+
const said = String(err.stderr ?? "").trim().split("\n").map((l) => l.trim()).filter(Boolean).slice(-2).join(" ").slice(0, 300);
|
|
242
|
+
const why = said || (err.message ?? "failed").split("\n")[0];
|
|
243
|
+
return `failed: claude mcp add did not work (${why}) — fix: run \`${command}\` yourself to see why, then pantheon install --user`;
|
|
201
244
|
}
|
|
202
245
|
function claudeUserScope(entry) {
|
|
203
246
|
const tool = "claude code";
|
|
@@ -211,11 +254,11 @@ function claudeUserScope(entry) {
|
|
|
211
254
|
execFileSync(remove.command, remove.args, { stdio: "ignore", ...remove.options });
|
|
212
255
|
}
|
|
213
256
|
catch { }
|
|
214
|
-
execFileSync(add.command, add.args, { stdio: "ignore", ...add.options });
|
|
257
|
+
execFileSync(add.command, add.args, { stdio: ["ignore", "ignore", "pipe"], ...add.options });
|
|
215
258
|
return { tool, where: "user scope", status: "ready in every project and every Conductor window" };
|
|
216
259
|
}
|
|
217
260
|
catch (e) {
|
|
218
|
-
return { tool, where: "user scope", status: `
|
|
261
|
+
return { tool, where: "user scope", status: claudeAddFailed(e, `claude mcp add pantheon -s user -- ${[entry.command, ...entry.args].join(" ")}`) };
|
|
219
262
|
}
|
|
220
263
|
}
|
|
221
264
|
function claudeLocalScope(entry, projectDir) {
|
|
@@ -230,11 +273,11 @@ function claudeLocalScope(entry, projectDir) {
|
|
|
230
273
|
execFileSync(remove.command, remove.args, { stdio: "ignore", cwd: projectDir, ...remove.options });
|
|
231
274
|
}
|
|
232
275
|
catch { }
|
|
233
|
-
execFileSync(add.command, add.args, { stdio: "ignore", cwd: projectDir, ...add.options });
|
|
276
|
+
execFileSync(add.command, add.args, { stdio: ["ignore", "ignore", "pipe"], cwd: projectDir, ...add.options });
|
|
234
277
|
return { tool, where: "local scope, this folder", status: "local override written — Claude Code starts pantheon here with this machine's node, and nothing in the repo changed" };
|
|
235
278
|
}
|
|
236
279
|
catch (e) {
|
|
237
|
-
return { tool, where: "local scope", status: `
|
|
280
|
+
return { tool, where: "local scope", status: claudeAddFailed(e, `claude mcp add pantheon -s local -- ${[entry.command, ...entry.args].join(" ")}`) };
|
|
238
281
|
}
|
|
239
282
|
}
|
|
240
283
|
export function agentPresence(home = homedir(), onPath = onPathNamed) {
|
|
@@ -328,7 +371,7 @@ const TOOL_TABLE = /^mcp_servers\.pantheon\.tools\.(pantheon_[a-z_]+)$/;
|
|
|
328
371
|
const OUR_SERVER_KEYS = /^\s*(command|args|env|default_tools_approval_mode)\s*=/;
|
|
329
372
|
function parseTables(current) {
|
|
330
373
|
const tables = [{ name: null, lines: [] }];
|
|
331
|
-
for (const line of current.split("\n")) {
|
|
374
|
+
for (const line of stripBom(current).replace(/\r\n/g, "\n").split("\n")) {
|
|
332
375
|
const h = HEADER.exec(line);
|
|
333
376
|
if (h)
|
|
334
377
|
tables.push({ name: h[1].trim(), lines: [line] });
|
|
@@ -506,8 +549,9 @@ export function upsertCodexToml(current, entry, approval = "approve") {
|
|
|
506
549
|
let body = out.join("\n").trimEnd();
|
|
507
550
|
if (!placed)
|
|
508
551
|
body = (body ? body + "\n\n" : "") + ourBlock().join("\n").trimEnd();
|
|
509
|
-
return body + "\n";
|
|
552
|
+
return sameShape(current, body + "\n");
|
|
510
553
|
}
|
|
554
|
+
const sameShape = (original, next) => (original.charCodeAt(0) === 0xfeff ? "\uFEFF" : "") + withEol(next, eolOf(original));
|
|
511
555
|
export function removeCodexToml(current) {
|
|
512
556
|
const out = [];
|
|
513
557
|
for (const t of parseTables(current)) {
|
|
@@ -528,7 +572,7 @@ export function removeCodexToml(current) {
|
|
|
528
572
|
out.push(...t.lines);
|
|
529
573
|
}
|
|
530
574
|
const body = out.join("\n").trimEnd();
|
|
531
|
-
return body ? body + "\n" : "";
|
|
575
|
+
return body ? sameShape(current, body + "\n") : "";
|
|
532
576
|
}
|
|
533
577
|
export const codexConfigFile = () => join(homedir(), ".codex", "config.toml");
|
|
534
578
|
export function codexApprovalOf(current) {
|
|
@@ -569,7 +613,10 @@ function codexUserScope(entry, approval, present = true) {
|
|
|
569
613
|
const cur = readFileSync(file, "utf8");
|
|
570
614
|
const had = codexApprovalOf(cur).present;
|
|
571
615
|
const kept = codexKept(cur, approval);
|
|
572
|
-
|
|
616
|
+
const next = upsertCodexToml(cur, entry, approval);
|
|
617
|
+
if (next === cur)
|
|
618
|
+
return { tool: "codex", where: file, status: `unchanged — ${said} is already there` };
|
|
619
|
+
writeFileSync(file, next);
|
|
573
620
|
const notes = [
|
|
574
621
|
kept.toolTables.length ? `kept your own [mcp_servers.pantheon.tools.*] settings for ${kept.toolTables.join(", ")}` : "",
|
|
575
622
|
kept.serverKeys.length ? `kept your own ${kept.serverKeys.join(", ")} in the server table` : "",
|
|
@@ -636,14 +683,17 @@ export function removeMcpJsonEntry(file, key = "mcpServers") {
|
|
|
636
683
|
try {
|
|
637
684
|
if (!existsSync(file))
|
|
638
685
|
return "absent";
|
|
639
|
-
const
|
|
686
|
+
const read = readConfigDoc(file);
|
|
687
|
+
const doc = read.doc;
|
|
640
688
|
const servers = doc[key];
|
|
641
689
|
if (!servers || !("pantheon" in servers))
|
|
642
690
|
return "absent";
|
|
643
691
|
if (!isOurMcpEntry(servers["pantheon"]))
|
|
644
692
|
return "not ours";
|
|
693
|
+
if (read.jsonc)
|
|
694
|
+
return `failed: it has comments or trailing commas (JSONC), so it was left exactly as it is`;
|
|
645
695
|
delete servers["pantheon"];
|
|
646
|
-
|
|
696
|
+
writeConfigJson(file, doc, read.text);
|
|
647
697
|
return "removed";
|
|
648
698
|
}
|
|
649
699
|
catch (e) {
|
package/dist/kiro-files.js
CHANGED
|
@@ -35,7 +35,7 @@ export function readKiroAgent(file, tracked = gitTracks) {
|
|
|
35
35
|
if (!existsSync(file))
|
|
36
36
|
return null;
|
|
37
37
|
try {
|
|
38
|
-
const doc = JSON.parse(readFileSync(file, "utf8"));
|
|
38
|
+
const doc = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
|
|
39
39
|
const hooks = Object.entries(doc.hooks && typeof doc.hooks === "object" ? doc.hooks : {});
|
|
40
40
|
const events = hooks.filter(([, es]) => Array.isArray(es) && es.some((h) => isOurKiroHook(h?.command))).map(([e]) => e);
|
|
41
41
|
const onlyOurs = hooks.every(([, es]) => Array.isArray(es) && es.every((h) => isOurKiroHook(h?.command)));
|
|
@@ -64,7 +64,7 @@ export function refreshKiroAgentAllowed(file, allowed) {
|
|
|
64
64
|
}
|
|
65
65
|
export function kiroDefaultIsPantheon(base = kiroDir()) {
|
|
66
66
|
try {
|
|
67
|
-
return JSON.parse(readFileSync(join(base, "settings", "cli.json"), "utf8"))["chat.defaultAgent"] === KIRO_AGENT_NAME;
|
|
67
|
+
return JSON.parse(readFileSync(join(base, "settings", "cli.json"), "utf8").replace(/^\uFEFF/, ""))["chat.defaultAgent"] === KIRO_AGENT_NAME;
|
|
68
68
|
}
|
|
69
69
|
catch {
|
|
70
70
|
return false;
|
package/dist/ledger.js
CHANGED
|
@@ -1,41 +1,121 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, existsSync, openSync, readSync, writeSync, fsyncSync, closeSync, fstatSync, copyFileSync, truncateSync, chmodSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { load } from "./config.js";
|
|
4
|
+
import { fsyncDir, renameRetrying, writeFileAtomic } from "./store.js";
|
|
5
|
+
export class SpaceFull extends Error {
|
|
6
|
+
bytes;
|
|
7
|
+
cap;
|
|
8
|
+
constructor(bytes, cap) {
|
|
9
|
+
super(`this team's history is at the relay's limit (${(bytes / 1e6).toFixed(1)} of ${(cap / 1e6).toFixed(1)} MB)`);
|
|
10
|
+
this.bytes = bytes;
|
|
11
|
+
this.cap = cap;
|
|
12
|
+
this.name = "SpaceFull";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
import { signedRead } from "./reads.js";
|
|
4
16
|
export class Ledger {
|
|
5
17
|
records = [];
|
|
18
|
+
byId = new Map();
|
|
6
19
|
seq = 0;
|
|
20
|
+
fileBytes = 0;
|
|
21
|
+
outOfOrder = 0;
|
|
22
|
+
firstOutOfOrder = null;
|
|
7
23
|
file;
|
|
8
24
|
constructor(dir, space) {
|
|
9
25
|
this.file = join(dir, `${space}.jsonl`);
|
|
10
|
-
mkdirSync(dirname(this.file), { recursive: true });
|
|
11
|
-
if (existsSync(this.file))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
27
|
+
if (existsSync(this.file))
|
|
28
|
+
this.read();
|
|
29
|
+
}
|
|
30
|
+
read() {
|
|
31
|
+
const fd = openSync(this.file, "r");
|
|
32
|
+
let size = 0, lastNl = -1;
|
|
33
|
+
try {
|
|
34
|
+
size = fstatSync(fd).size;
|
|
35
|
+
const buf = Buffer.allocUnsafe(1 << 20);
|
|
36
|
+
let pos = 0;
|
|
37
|
+
let carry = [];
|
|
38
|
+
for (let n = readSync(fd, buf, 0, buf.length, pos); n > 0; n = readSync(fd, buf, 0, buf.length, pos)) {
|
|
39
|
+
let start = 0;
|
|
40
|
+
for (let i = buf.indexOf(10, 0); i !== -1 && i < n; i = buf.indexOf(10, i + 1)) {
|
|
41
|
+
const part = buf.subarray(start, i);
|
|
42
|
+
const line = carry.length ? Buffer.concat([...carry, part]).toString("utf8") : part.toString("utf8");
|
|
43
|
+
carry = [];
|
|
44
|
+
this.take(line);
|
|
45
|
+
start = i + 1;
|
|
46
|
+
lastNl = pos + i;
|
|
28
47
|
}
|
|
48
|
+
if (start < n)
|
|
49
|
+
carry.push(Buffer.from(buf.subarray(start, n)));
|
|
50
|
+
pos += n;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
this.fileBytes = size;
|
|
57
|
+
const keep = lastNl + 1;
|
|
58
|
+
if (keep < size) {
|
|
59
|
+
const tmp = `${this.file}.tmp`;
|
|
60
|
+
copyFileSync(this.file, tmp);
|
|
61
|
+
truncateSync(tmp, keep);
|
|
62
|
+
const fd2 = openSync(tmp, "r+");
|
|
63
|
+
try {
|
|
64
|
+
fsyncSync(fd2);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
closeSync(fd2);
|
|
29
68
|
}
|
|
69
|
+
renameRetrying(tmp, this.file);
|
|
70
|
+
fsyncDir(dirname(this.file));
|
|
71
|
+
this.fileBytes = keep;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
chmodSync(this.file, 0o600);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
}
|
|
78
|
+
take(line) {
|
|
79
|
+
if (!line.trim())
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
const r = JSON.parse(line);
|
|
83
|
+
this.records.push(r);
|
|
84
|
+
if (typeof r.id === "string" && !this.byId.has(r.id))
|
|
85
|
+
this.byId.set(r.id, r);
|
|
86
|
+
if (r.seq > this.seq)
|
|
87
|
+
this.seq = r.seq;
|
|
88
|
+
else if (this.records.length > 1) {
|
|
89
|
+
this.outOfOrder++;
|
|
90
|
+
this.firstOutOfOrder ??= r.seq;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
30
94
|
}
|
|
31
95
|
}
|
|
32
96
|
get head() {
|
|
33
97
|
return this.seq;
|
|
34
98
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
99
|
+
get bytes() {
|
|
100
|
+
return this.fileBytes;
|
|
101
|
+
}
|
|
102
|
+
append(rec, cap = 0) {
|
|
103
|
+
const full = { ...rec, seq: this.seq + 1 };
|
|
104
|
+
const line = Buffer.from(JSON.stringify(full) + "\n");
|
|
105
|
+
if (cap > 0 && this.fileBytes + line.length > cap)
|
|
106
|
+
throw new SpaceFull(this.fileBytes, cap);
|
|
107
|
+
const fd = openSync(this.file, "a", 0o600);
|
|
108
|
+
try {
|
|
109
|
+
writeSync(fd, line);
|
|
110
|
+
fsyncSync(fd);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
this.seq = full.seq;
|
|
116
|
+
this.fileBytes += line.length;
|
|
38
117
|
this.records.push(full);
|
|
118
|
+
this.byId.set(full.id, full);
|
|
39
119
|
return full;
|
|
40
120
|
}
|
|
41
121
|
since(seq, limit = 500) {
|
|
@@ -49,7 +129,7 @@ export class Ledger {
|
|
|
49
129
|
return hits.slice(Math.max(0, hits.length - limit));
|
|
50
130
|
}
|
|
51
131
|
find(id) {
|
|
52
|
-
return this.
|
|
132
|
+
return this.byId.get(id);
|
|
53
133
|
}
|
|
54
134
|
all() {
|
|
55
135
|
return this.records;
|
|
@@ -65,10 +145,14 @@ export class Ledger {
|
|
|
65
145
|
const dropped = this.records.length - kept.length;
|
|
66
146
|
if (dropped === 0)
|
|
67
147
|
return { dropped: 0, kept: kept.length };
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
renameSync(tmp, this.file);
|
|
148
|
+
const text = kept.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
149
|
+
writeFileAtomic(this.file, text);
|
|
71
150
|
this.records = kept;
|
|
151
|
+
this.byId = new Map();
|
|
152
|
+
for (const r of kept)
|
|
153
|
+
if (!this.byId.has(r.id))
|
|
154
|
+
this.byId.set(r.id, r);
|
|
155
|
+
this.fileBytes = Buffer.byteLength(text);
|
|
72
156
|
return { dropped, kept: kept.length };
|
|
73
157
|
}
|
|
74
158
|
}
|
|
@@ -105,10 +189,9 @@ export async function findDelivered(actId, opts) {
|
|
|
105
189
|
if (!cfg)
|
|
106
190
|
throw new LedgerUnreachable("no pantheon config — run `pantheon join` or `pantheon init` first");
|
|
107
191
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
108
|
-
const url = `${base}/history?space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
109
192
|
let acts;
|
|
110
193
|
try {
|
|
111
|
-
const res = await
|
|
194
|
+
const res = await signedRead(cfg, "/history");
|
|
112
195
|
if (!res.ok)
|
|
113
196
|
throw new LedgerUnreachable(`relay ${base} answered ${res.status} for /history`);
|
|
114
197
|
acts = (await res.json()).acts ?? [];
|