@join-pantheon/cli 0.33.3 → 0.33.4-rc.1
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/dist/accounts.js +53 -0
- package/dist/board.js +1 -2
- package/dist/cli.js +183 -22
- package/dist/config.js +2 -1
- package/dist/disk-writer.js +404 -0
- package/dist/doctor.js +6 -3
- package/dist/ledger.js +31 -14
- package/dist/notifier.js +1 -1
- package/dist/quickstart.js +18 -6
- package/dist/recover-offer.js +125 -0
- package/dist/relay.js +258 -74
- package/dist/roster-file.js +11 -6
- package/dist/store.js +28 -7
- package/dist/team-name.js +58 -0
- package/dist/tray.js +5 -0
- package/dist/worker.js +5 -3
- package/helper/macos/main.swift +5 -3
- package/helper/macos/prebuilt/manifest.json +3 -3
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/installer/install.ps1 +12 -2
- package/installer/install.sh +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { promises as fsp, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { noteDiskWait, slowDiskMs, waitsForDisk } from "./store.js";
|
|
4
|
+
export function supersedeFor(file) {
|
|
5
|
+
return file.endsWith(".state.json") ? "mark" : file.endsWith(".jsonl") ? "first" : "carry";
|
|
6
|
+
}
|
|
7
|
+
export class DiskBusy extends Error {
|
|
8
|
+
bytes;
|
|
9
|
+
cap;
|
|
10
|
+
code = "STORAGE_BUSY";
|
|
11
|
+
constructor(bytes, cap) {
|
|
12
|
+
super(`the relay's disk is ${(bytes / 1e6).toFixed(1)} MB behind on this team's writes`);
|
|
13
|
+
this.bytes = bytes;
|
|
14
|
+
this.cap = cap;
|
|
15
|
+
this.name = "DiskBusy";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function backlogCap() {
|
|
19
|
+
const n = Number(process.env.PANTHEON_RELAY_WRITE_BACKLOG_BYTES ?? "");
|
|
20
|
+
return Number.isFinite(n) && n > 0 ? n : 8 * 1024 * 1024;
|
|
21
|
+
}
|
|
22
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
const APPEND_HOLD_MS = 2_000;
|
|
24
|
+
export function retryDelayMs(attempt) {
|
|
25
|
+
return Math.min(5_000, 50 * 2 ** Math.max(0, attempt - 1));
|
|
26
|
+
}
|
|
27
|
+
const reporters = new Set();
|
|
28
|
+
export function onWriteError(dir, fn) {
|
|
29
|
+
const r = { dir: resolve(dir), fn };
|
|
30
|
+
reporters.add(r);
|
|
31
|
+
return () => { reporters.delete(r); };
|
|
32
|
+
}
|
|
33
|
+
export class SpaceWriter {
|
|
34
|
+
dir;
|
|
35
|
+
stem;
|
|
36
|
+
key;
|
|
37
|
+
queue = [];
|
|
38
|
+
inflight = null;
|
|
39
|
+
issued = 0;
|
|
40
|
+
open = new Set();
|
|
41
|
+
waiters = [];
|
|
42
|
+
running = false;
|
|
43
|
+
failing = null;
|
|
44
|
+
backlogBytes = 0;
|
|
45
|
+
constructor(dir, stem, key) {
|
|
46
|
+
this.dir = dir;
|
|
47
|
+
this.stem = stem;
|
|
48
|
+
this.key = key;
|
|
49
|
+
}
|
|
50
|
+
get ticket() { return this.issued; }
|
|
51
|
+
get doneThrough() {
|
|
52
|
+
let low = Infinity;
|
|
53
|
+
for (const t of this.open)
|
|
54
|
+
if (t < low)
|
|
55
|
+
low = t;
|
|
56
|
+
return low === Infinity ? this.issued : low - 1;
|
|
57
|
+
}
|
|
58
|
+
get idle() { return !this.inflight && this.queue.length === 0; }
|
|
59
|
+
whenWritten(ticket = this.issued) {
|
|
60
|
+
if (this.doneThrough >= ticket)
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
return new Promise((done) => this.waiters.push({ ticket, done }));
|
|
63
|
+
}
|
|
64
|
+
append(file, data, offset) {
|
|
65
|
+
return this.push({ kind: "append", file, data, offset, tickets: [] });
|
|
66
|
+
}
|
|
67
|
+
replace(file, data, mode = 0o600, how = supersedeFor(file)) {
|
|
68
|
+
return this.push({ kind: "replace", file, data: typeof data === "string" ? Buffer.from(data) : data, mode, tickets: [] }, how);
|
|
69
|
+
}
|
|
70
|
+
remove(file) {
|
|
71
|
+
if (this.idle && this.appendHandle?.file !== file) {
|
|
72
|
+
try {
|
|
73
|
+
rmSync(file, { force: true });
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
return this.issued;
|
|
77
|
+
}
|
|
78
|
+
return this.push({ kind: "remove", file, tickets: [] });
|
|
79
|
+
}
|
|
80
|
+
push(op, how = "carry") {
|
|
81
|
+
const t = ++this.issued;
|
|
82
|
+
this.open.add(t);
|
|
83
|
+
op.tickets.push(t);
|
|
84
|
+
if ("data" in op)
|
|
85
|
+
this.backlogBytes += op.data.length;
|
|
86
|
+
for (const r of recorders)
|
|
87
|
+
r.set(this, t);
|
|
88
|
+
if (op.kind === "append")
|
|
89
|
+
this.queue.push(op);
|
|
90
|
+
else {
|
|
91
|
+
const next = [];
|
|
92
|
+
let at = -1;
|
|
93
|
+
for (const [i, q] of this.queue.entries()) {
|
|
94
|
+
if (q.file !== op.file || q.kind === "noop") {
|
|
95
|
+
next.push(q);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (how === "mark" && i === 0) {
|
|
99
|
+
next.push(q);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if ("data" in q)
|
|
103
|
+
this.backlogBytes -= q.data.length;
|
|
104
|
+
if (how === "mark") {
|
|
105
|
+
next.push({ kind: "noop", file: q.file, tickets: q.tickets });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
op.tickets.push(...q.tickets);
|
|
109
|
+
if (how === "first" && at < 0)
|
|
110
|
+
at = next.length;
|
|
111
|
+
}
|
|
112
|
+
if (at < 0)
|
|
113
|
+
next.push(op);
|
|
114
|
+
else
|
|
115
|
+
next.splice(at, 0, op);
|
|
116
|
+
this.queue = next;
|
|
117
|
+
}
|
|
118
|
+
if (!this.running)
|
|
119
|
+
void this.pump();
|
|
120
|
+
return t;
|
|
121
|
+
}
|
|
122
|
+
view(file) {
|
|
123
|
+
const ops = [this.inflight, ...this.queue].filter((o) => !!o && o.file === file && o.kind !== "noop");
|
|
124
|
+
if (!ops.length)
|
|
125
|
+
return undefined;
|
|
126
|
+
let i = ops.length - 1;
|
|
127
|
+
while (i >= 0 && ops[i].kind === "append")
|
|
128
|
+
i--;
|
|
129
|
+
const tail = ops.slice(i + 1).map((o) => o.data);
|
|
130
|
+
let head;
|
|
131
|
+
if (i >= 0) {
|
|
132
|
+
const last = ops[i];
|
|
133
|
+
if (last.kind === "remove" && !tail.length)
|
|
134
|
+
return null;
|
|
135
|
+
head = last.kind === "replace" ? last.data : Buffer.alloc(0);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const first = ops[0];
|
|
139
|
+
let disk = Buffer.alloc(0);
|
|
140
|
+
try {
|
|
141
|
+
disk = readFileSync(file);
|
|
142
|
+
}
|
|
143
|
+
catch { }
|
|
144
|
+
head = disk.subarray(0, first.offset);
|
|
145
|
+
}
|
|
146
|
+
return Buffer.concat([head, ...tail]);
|
|
147
|
+
}
|
|
148
|
+
async pump() {
|
|
149
|
+
this.running = true;
|
|
150
|
+
try {
|
|
151
|
+
while (this.queue.length) {
|
|
152
|
+
let op = this.queue.shift();
|
|
153
|
+
if (op.kind === "append") {
|
|
154
|
+
const parts = [op.data];
|
|
155
|
+
let end = op.offset + op.data.length;
|
|
156
|
+
const tickets = [...op.tickets];
|
|
157
|
+
for (let q = this.queue[0]; q && (q.kind === "noop" || (q.kind === "append" && q.file === op.file && q.offset === end)); q = this.queue[0]) {
|
|
158
|
+
this.queue.shift();
|
|
159
|
+
tickets.push(...q.tickets);
|
|
160
|
+
if (q.kind === "append") {
|
|
161
|
+
parts.push(q.data);
|
|
162
|
+
end += q.data.length;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (tickets.length > op.tickets.length)
|
|
166
|
+
op = { kind: "append", file: op.file, data: Buffer.concat(parts), offset: op.offset, tickets };
|
|
167
|
+
}
|
|
168
|
+
this.inflight = op;
|
|
169
|
+
for (let attempt = 0; op.kind !== "noop"; attempt++) {
|
|
170
|
+
try {
|
|
171
|
+
const slow = slowDiskMs();
|
|
172
|
+
if (slow)
|
|
173
|
+
await sleep(slow);
|
|
174
|
+
await this.perform(op, attempt);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
this.failing = e;
|
|
179
|
+
if (attempt < 3 || attempt % 12 === 0)
|
|
180
|
+
report(this.dir, this.stem, `writing ${op.file}${attempt ? ` (attempt ${attempt + 1})` : ""}`, e);
|
|
181
|
+
await sleep(retryDelayMs(attempt + 1));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.failing = null;
|
|
185
|
+
if ("data" in op)
|
|
186
|
+
this.backlogBytes -= op.data.length;
|
|
187
|
+
this.inflight = null;
|
|
188
|
+
for (const t of op.tickets)
|
|
189
|
+
this.open.delete(t);
|
|
190
|
+
this.wake();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
this.running = false;
|
|
195
|
+
this.forgetIfDone();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
forget() { this.forgetIfDone(); }
|
|
199
|
+
forgetIfDone() {
|
|
200
|
+
if (this.idle && !this.running && !this.appendHandle && writers.get(this.key) === this)
|
|
201
|
+
writers.delete(this.key);
|
|
202
|
+
}
|
|
203
|
+
appendHandle = null;
|
|
204
|
+
async closeAppendHandle() {
|
|
205
|
+
const h = this.appendHandle;
|
|
206
|
+
if (!h)
|
|
207
|
+
return;
|
|
208
|
+
this.appendHandle = null;
|
|
209
|
+
clearTimeout(h.timer);
|
|
210
|
+
await h.fh.close().catch(() => { });
|
|
211
|
+
}
|
|
212
|
+
async perform(op, attempt) {
|
|
213
|
+
if (this.appendHandle && (op.kind !== "append" || op.file !== this.appendHandle.file || attempt > 0))
|
|
214
|
+
await this.closeAppendHandle();
|
|
215
|
+
if (op.kind === "remove") {
|
|
216
|
+
await fsp.rm(op.file, { force: true });
|
|
217
|
+
await fsp.rm(`${op.file}.tmp`, { force: true });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (op.kind === "replace")
|
|
221
|
+
return replaceAtomic(op.file, op.data, op.mode);
|
|
222
|
+
if (op.kind === "noop")
|
|
223
|
+
return;
|
|
224
|
+
if (attempt > 0) {
|
|
225
|
+
try {
|
|
226
|
+
const st = await fsp.stat(op.file);
|
|
227
|
+
if (st.size > op.offset)
|
|
228
|
+
await fsp.truncate(op.file, op.offset);
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
if (e.code !== "ENOENT")
|
|
232
|
+
throw e;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!this.appendHandle)
|
|
236
|
+
this.appendHandle = { file: op.file, fh: await fsp.open(op.file, "a", 0o600) };
|
|
237
|
+
const h = this.appendHandle;
|
|
238
|
+
clearTimeout(h.timer);
|
|
239
|
+
try {
|
|
240
|
+
await writeAll(h.fh, op.data);
|
|
241
|
+
await syncHandle(h.fh);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
await this.closeAppendHandle();
|
|
245
|
+
throw e;
|
|
246
|
+
}
|
|
247
|
+
if (process.platform === "win32")
|
|
248
|
+
return this.closeAppendHandle();
|
|
249
|
+
h.timer = setTimeout(() => {
|
|
250
|
+
if (this.appendHandle !== h || this.inflight)
|
|
251
|
+
return;
|
|
252
|
+
void this.closeAppendHandle().then(() => this.forgetIfDone());
|
|
253
|
+
}, APPEND_HOLD_MS);
|
|
254
|
+
h.timer.unref?.();
|
|
255
|
+
}
|
|
256
|
+
wake() {
|
|
257
|
+
const through = this.doneThrough;
|
|
258
|
+
const ready = this.waiters.filter((w) => w.ticket <= through);
|
|
259
|
+
if (!ready.length)
|
|
260
|
+
return;
|
|
261
|
+
this.waiters = this.waiters.filter((w) => w.ticket > through);
|
|
262
|
+
for (const w of ready)
|
|
263
|
+
w.done();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function report(dir, stem, where, e) {
|
|
267
|
+
let heard = false;
|
|
268
|
+
for (const r of reporters) {
|
|
269
|
+
if (r.dir !== dir)
|
|
270
|
+
continue;
|
|
271
|
+
heard = true;
|
|
272
|
+
try {
|
|
273
|
+
r.fn(where, e, stem);
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
}
|
|
277
|
+
if (!heard)
|
|
278
|
+
console.error(`! storage: ${where} failed: ${e.message}`);
|
|
279
|
+
}
|
|
280
|
+
async function writeAll(fh, data) {
|
|
281
|
+
for (let off = 0; off < data.length;) {
|
|
282
|
+
const { bytesWritten } = await fh.write(data, off, data.length - off);
|
|
283
|
+
off += bytesWritten;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function syncHandle(fh) {
|
|
287
|
+
if (!waitsForDisk())
|
|
288
|
+
return;
|
|
289
|
+
const t = Date.now();
|
|
290
|
+
await fh.sync();
|
|
291
|
+
noteDiskWait(Date.now() - t);
|
|
292
|
+
}
|
|
293
|
+
async function replaceAtomic(file, data, mode) {
|
|
294
|
+
const tmp = `${file}.tmp`;
|
|
295
|
+
let fh;
|
|
296
|
+
try {
|
|
297
|
+
fh = await fsp.open(tmp, "w", mode);
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
if (e.code !== "ENOENT")
|
|
301
|
+
throw e;
|
|
302
|
+
await fsp.mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
303
|
+
fh = await fsp.open(tmp, "w", mode);
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await writeAll(fh, data);
|
|
307
|
+
await syncHandle(fh);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
await fh.close();
|
|
311
|
+
}
|
|
312
|
+
await renameRetrying(tmp, file);
|
|
313
|
+
if (waitsForDisk()) {
|
|
314
|
+
let dh = null;
|
|
315
|
+
try {
|
|
316
|
+
dh = await fsp.open(dirname(file), "r");
|
|
317
|
+
await syncHandle(dh);
|
|
318
|
+
}
|
|
319
|
+
catch { }
|
|
320
|
+
finally {
|
|
321
|
+
await dh?.close().catch(() => { });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function renameRetrying(from, to) {
|
|
326
|
+
for (let i = 0;; i++) {
|
|
327
|
+
try {
|
|
328
|
+
await fsp.rename(from, to);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
if (process.platform !== "win32" || i >= 20 || !["EPERM", "EACCES", "EBUSY"].includes(e.code ?? ""))
|
|
333
|
+
throw e;
|
|
334
|
+
await sleep(10);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const writers = new Map();
|
|
339
|
+
const keyOf = (dir, stem) => join(resolve(dir), stem);
|
|
340
|
+
export function writerFor(dir, stem) {
|
|
341
|
+
const key = keyOf(dir, stem);
|
|
342
|
+
let w = writers.get(key);
|
|
343
|
+
if (!w) {
|
|
344
|
+
w = new SpaceWriter(resolve(dir), stem, key);
|
|
345
|
+
writers.set(key, w);
|
|
346
|
+
}
|
|
347
|
+
return w;
|
|
348
|
+
}
|
|
349
|
+
export function busyWriter(dir, stem) {
|
|
350
|
+
const w = writers.get(keyOf(dir, stem));
|
|
351
|
+
return w && !w.idle ? w : null;
|
|
352
|
+
}
|
|
353
|
+
export function pendingView(dir, stem, file) {
|
|
354
|
+
return busyWriter(dir, stem)?.view(file);
|
|
355
|
+
}
|
|
356
|
+
export function readLatest(dir, stem, file) {
|
|
357
|
+
const v = pendingView(dir, stem, file);
|
|
358
|
+
if (v === undefined)
|
|
359
|
+
return readFileSync(file, "utf8");
|
|
360
|
+
if (v === null)
|
|
361
|
+
throw Object.assign(new Error(`ENOENT: no such file, open '${file}'`), { code: "ENOENT" });
|
|
362
|
+
return v.toString("utf8");
|
|
363
|
+
}
|
|
364
|
+
export async function flushWrites(dir, timeoutMs = 30_000) {
|
|
365
|
+
const root = dir ? resolve(dir) : null;
|
|
366
|
+
const end = Date.now() + timeoutMs;
|
|
367
|
+
for (;;) {
|
|
368
|
+
const busy = [...writers.values()].filter((w) => !w.idle && (!root || w.dir === root));
|
|
369
|
+
if (!busy.length) {
|
|
370
|
+
await Promise.all([...writers.values()].filter((w) => !root || w.dir === root).map((w) => w.closeAppendHandle()));
|
|
371
|
+
for (const w of [...writers.values()])
|
|
372
|
+
if (w.idle && (!root || w.dir === root))
|
|
373
|
+
w.forget();
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
const left = end - Date.now();
|
|
377
|
+
if (left <= 0)
|
|
378
|
+
return false;
|
|
379
|
+
let timer;
|
|
380
|
+
await Promise.race([Promise.all(busy.map((w) => w.whenWritten())), new Promise((r) => { timer = setTimeout(r, left); })]);
|
|
381
|
+
clearTimeout(timer);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const recorders = new Set();
|
|
385
|
+
export function recordWrites() {
|
|
386
|
+
const got = new Map();
|
|
387
|
+
recorders.add(got);
|
|
388
|
+
const t = setImmediate(() => recorders.delete(got));
|
|
389
|
+
return () => { clearImmediate(t); recorders.delete(got); return got; };
|
|
390
|
+
}
|
|
391
|
+
export async function whenLanded(writes, timeoutMs = 10_000) {
|
|
392
|
+
const waits = [...writes].map(([w, t]) => ({ w, t }));
|
|
393
|
+
const end = Date.now() + timeoutMs;
|
|
394
|
+
for (;;) {
|
|
395
|
+
if (waits.every(({ w, t }) => w.doneThrough >= t))
|
|
396
|
+
return true;
|
|
397
|
+
if (waits.some(({ w, t }) => w.failing && w.doneThrough < t) || Date.now() >= end)
|
|
398
|
+
return false;
|
|
399
|
+
let timer;
|
|
400
|
+
await Promise.race([Promise.all(waits.map(({ w, t }) => w.whenWritten(t))), new Promise((r) => { timer = setTimeout(r, 100); })]);
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
//# sourceMappingURL=disk-writer.js.map
|
package/dist/doctor.js
CHANGED
|
@@ -15,7 +15,7 @@ import { expectedLauncher, managedLayout, runtimeNodeFit, runtimeVerdict } from
|
|
|
15
15
|
import { compareVersions } from "./installer.js";
|
|
16
16
|
import { PKG_NAME } from "./upgrade.js";
|
|
17
17
|
import { PantheonClient } from "./client.js";
|
|
18
|
-
import { load, resolveFor, loadAll, CONFIG_PATH } from "./config.js";
|
|
18
|
+
import { load, resolveFor, loadAll, configFor, CONFIG_PATH } from "./config.js";
|
|
19
19
|
import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
|
|
20
20
|
import { approvalSummary, chosenApproval, copilotMcpConfigFile, kiroMcpConfigFile, onPath } from "./install.js";
|
|
21
21
|
import { hookState, readIntent, agentHookStates, repairNote } from "./notify.js";
|
|
@@ -204,7 +204,7 @@ export async function doctor(projectDir) {
|
|
|
204
204
|
}
|
|
205
205
|
let cfg = null;
|
|
206
206
|
try {
|
|
207
|
-
cfg = load();
|
|
207
|
+
cfg = load() ?? (() => { const f = loadAll(); const m = Object.values(f?.spaces ?? {})[0]; return f && m ? configFor(f, m, null) : null; })();
|
|
208
208
|
}
|
|
209
209
|
catch (e) {
|
|
210
210
|
add("fail", "config", `unreadable: ${e.message}`, undefined, "unreadable");
|
|
@@ -215,7 +215,10 @@ export async function doctor(projectDir) {
|
|
|
215
215
|
}
|
|
216
216
|
return out;
|
|
217
217
|
}
|
|
218
|
-
|
|
218
|
+
const teamNames = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name);
|
|
219
|
+
add("pass", "config", resolveFor(projectDir).cfg || teamNames.length < 2
|
|
220
|
+
? `${cfg.member} in "${cfg.space}" via ${cfg.relay} (${CONFIG_PATH})`
|
|
221
|
+
: `${cfg.member} in ${teamNames.length} teams (${teamNames.join(", ")}), none picked for this folder (${CONFIG_PATH})`, undefined, "present");
|
|
219
222
|
const here = resolveFor(projectDir, { strict: true });
|
|
220
223
|
const nTeams = Object.keys(loadAll()?.spaces ?? {}).length;
|
|
221
224
|
const folderHere = here.reason === "binding" && !here.repo;
|
package/dist/ledger.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { mkdirSync, existsSync, openSync, readSync,
|
|
1
|
+
import { mkdirSync, existsSync, openSync, readSync, 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
|
|
4
|
+
import { fsyncDir, renameRetrying } from "./store.js";
|
|
5
|
+
import { backlogCap, busyWriter, DiskBusy, pendingView, writerFor } from "./disk-writer.js";
|
|
5
6
|
export class SpaceFull extends Error {
|
|
6
7
|
bytes;
|
|
7
8
|
cap;
|
|
@@ -14,6 +15,8 @@ export class SpaceFull extends Error {
|
|
|
14
15
|
}
|
|
15
16
|
import { signedRead } from "./reads.js";
|
|
16
17
|
export class Ledger {
|
|
18
|
+
dir;
|
|
19
|
+
stem;
|
|
17
20
|
records = [];
|
|
18
21
|
byId = new Map();
|
|
19
22
|
seq = 0;
|
|
@@ -21,12 +24,25 @@ export class Ledger {
|
|
|
21
24
|
outOfOrder = 0;
|
|
22
25
|
firstOutOfOrder = null;
|
|
23
26
|
file;
|
|
24
|
-
constructor(dir,
|
|
25
|
-
this.
|
|
27
|
+
constructor(dir, stem) {
|
|
28
|
+
this.dir = dir;
|
|
29
|
+
this.stem = stem;
|
|
30
|
+
this.file = join(dir, `${stem}.jsonl`);
|
|
26
31
|
mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
27
|
-
|
|
32
|
+
const pending = pendingView(dir, stem, this.file);
|
|
33
|
+
if (pending !== undefined)
|
|
34
|
+
this.readPending(pending ?? Buffer.alloc(0));
|
|
35
|
+
else if (existsSync(this.file))
|
|
28
36
|
this.read();
|
|
29
37
|
}
|
|
38
|
+
readPending(buf) {
|
|
39
|
+
let start = 0;
|
|
40
|
+
for (let i = buf.indexOf(10); i !== -1; i = buf.indexOf(10, start)) {
|
|
41
|
+
this.take(buf.subarray(start, i).toString("utf8"));
|
|
42
|
+
start = i + 1;
|
|
43
|
+
}
|
|
44
|
+
this.fileBytes = buf.length;
|
|
45
|
+
}
|
|
30
46
|
read() {
|
|
31
47
|
const fd = openSync(this.file, "r");
|
|
32
48
|
let size = 0, lastNl = -1;
|
|
@@ -100,24 +116,25 @@ export class Ledger {
|
|
|
100
116
|
return this.fileBytes;
|
|
101
117
|
}
|
|
102
118
|
append(rec, cap = 0) {
|
|
119
|
+
const w = writerFor(this.dir, this.stem);
|
|
120
|
+
if (w.failing)
|
|
121
|
+
throw w.failing;
|
|
103
122
|
const full = { ...rec, seq: this.seq + 1 };
|
|
104
123
|
const line = Buffer.from(JSON.stringify(full) + "\n");
|
|
105
124
|
if (cap > 0 && this.fileBytes + line.length > cap)
|
|
106
125
|
throw new SpaceFull(this.fileBytes, cap);
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
writeSync(fd, line);
|
|
110
|
-
syncFd(fd);
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
closeSync(fd);
|
|
114
|
-
}
|
|
126
|
+
w.append(this.file, line, this.fileBytes);
|
|
115
127
|
this.seq = full.seq;
|
|
116
128
|
this.fileBytes += line.length;
|
|
117
129
|
this.records.push(full);
|
|
118
130
|
this.byId.set(full.id, full);
|
|
119
131
|
return full;
|
|
120
132
|
}
|
|
133
|
+
checkBacklog() {
|
|
134
|
+
const w = busyWriter(this.dir, this.stem);
|
|
135
|
+
if (w && w.backlogBytes > backlogCap())
|
|
136
|
+
throw new DiskBusy(w.backlogBytes, backlogCap());
|
|
137
|
+
}
|
|
121
138
|
since(seq, limit = 500) {
|
|
122
139
|
return this.records.filter((r) => r.seq > seq).slice(0, limit);
|
|
123
140
|
}
|
|
@@ -146,7 +163,7 @@ export class Ledger {
|
|
|
146
163
|
if (dropped === 0)
|
|
147
164
|
return { dropped: 0, kept: kept.length };
|
|
148
165
|
const text = kept.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
149
|
-
|
|
166
|
+
writerFor(this.dir, this.stem).replace(this.file, text);
|
|
150
167
|
this.records = kept;
|
|
151
168
|
this.byId = new Map();
|
|
152
169
|
for (const r of kept)
|
package/dist/notifier.js
CHANGED
|
@@ -167,7 +167,7 @@ export class HumanNotifier {
|
|
|
167
167
|
const peerOf = (from) => from ? { member: from, agent: client.roster.find((m) => m.id === from)?.agent } : undefined;
|
|
168
168
|
const withLabel = (n) => {
|
|
169
169
|
const labelled = this.opts.label ? { ...n, context: n.context ? `${this.opts.label} · ${n.context}` : this.opts.label } : n;
|
|
170
|
-
return
|
|
170
|
+
return { ...labelled, space: this.cfg.space };
|
|
171
171
|
};
|
|
172
172
|
let asks = {};
|
|
173
173
|
try {
|
package/dist/quickstart.js
CHANGED
|
@@ -9,6 +9,7 @@ import { PantheonClient } from "./client.js";
|
|
|
9
9
|
import { holdsRepo } from "./repo-find.js";
|
|
10
10
|
import { folderFacts, folderRefusal, folderRefusalHere, isInside, isTemporaryRefusal } from "./folder-guard.js";
|
|
11
11
|
import { load, loadAll, identity, save, toInvite, resolveFor, repoInfo, bindRepo, bindFolder, folderBinding, DEFAULT_RELAY } from "./config.js";
|
|
12
|
+
import { looksLikePlaceholder, teamSlug } from "./team-name.js";
|
|
12
13
|
import { commandsChosen, loadProfile, wakeChosen, wakeDefault } from "./profile.js";
|
|
13
14
|
import { newKeypair } from "./identity.js";
|
|
14
15
|
import { installForUser } from "./install.js";
|
|
@@ -268,7 +269,16 @@ export function deviceLabel(plat = platform(), env = process.env, info, host = h
|
|
|
268
269
|
}
|
|
269
270
|
export function teamNameFor(dir, repo = repoInfo(dir)) {
|
|
270
271
|
const fromOrigin = repo?.origin ? (repo.origin.replace(/\.git$/i, "").split(/[/:]/).filter(Boolean).pop() ?? "") : "";
|
|
271
|
-
|
|
272
|
+
const slug = teamSlug(slugName(fromOrigin || basename(repo?.root ?? dir), "team")) || "team";
|
|
273
|
+
if (!looksLikePlaceholder(slug))
|
|
274
|
+
return slug;
|
|
275
|
+
return ["team", "project", "folder"].map((w) => teamSlug(`${slug}-${w}`)).find((n) => n && !looksLikePlaceholder(n)) ?? "team";
|
|
276
|
+
}
|
|
277
|
+
export function startReconnects(opts, here = resolveFor(process.cwd())) {
|
|
278
|
+
const existing = here.cfg;
|
|
279
|
+
return !!existing && !opts.reset && !!existing.space && !!existing.token
|
|
280
|
+
&& (here.reason === "binding" || here.reason === "single" || here.reason === "env")
|
|
281
|
+
&& (!opts.space || opts.space === existing.space);
|
|
272
282
|
}
|
|
273
283
|
export function isLocalRelay(url) {
|
|
274
284
|
let host = "";
|
|
@@ -421,6 +431,7 @@ export async function start(opts) {
|
|
|
421
431
|
: { url: chosen, how: place === "this-machine" ? "this machine's address" : place === "tunnel" ? "a tunnel to this machine" : opts.relay ? "as given" : "your signed-in relay", shareable: true })
|
|
422
432
|
: findReach(opts.port);
|
|
423
433
|
const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, inviteMayWake: false, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
|
|
434
|
+
const reuse = startReconnects(opts, here);
|
|
424
435
|
let relay = null;
|
|
425
436
|
if (chosen && !hostHere) {
|
|
426
437
|
const probe = await probeRelay(reach.url, opts.remoteTimeoutMs ?? 10_000, opts.fetchImpl);
|
|
@@ -453,9 +464,6 @@ export async function start(opts) {
|
|
|
453
464
|
rememberHosted(opts.dataDir, chosen);
|
|
454
465
|
}
|
|
455
466
|
const existing = priorCfg;
|
|
456
|
-
const hasOwnSpace = !!existing?.space && !!existing?.token;
|
|
457
|
-
const reuse = !!existing && !opts.reset && hasOwnSpace && (here.reason === "binding" || here.reason === "single" || here.reason === "env")
|
|
458
|
-
&& (!opts.space || opts.space === existing.space);
|
|
459
467
|
const keepIdentity = !!existing && !opts.reset && !!existing.publicKey && !!existing.privateKey;
|
|
460
468
|
let cfg = reuse
|
|
461
469
|
? { ...existing, relay: reach.url, agent: detectAgent() }
|
|
@@ -534,8 +542,12 @@ export async function start(opts) {
|
|
|
534
542
|
catch (e) {
|
|
535
543
|
const msg = (await interceptedText(e, cfg.relay)) ?? e.message;
|
|
536
544
|
const nameTaken = !reuse && /BAD_TOKEN/.test(msg);
|
|
537
|
-
|
|
538
|
-
|
|
545
|
+
const ownSeat = nameTaken && /already a member of/i.test(msg);
|
|
546
|
+
if (ownSeat) {
|
|
547
|
+
warnings.push(msg.replace(/^BAD_TOKEN:\s*/, ""));
|
|
548
|
+
}
|
|
549
|
+
else if (nameTaken && !opts.space) {
|
|
550
|
+
const alt = teamSlug(`${cfg.space}-${cfg.member}`) || cfg.space;
|
|
539
551
|
opts.log(` space "${cfg.space}" is already another team's space on this relay — using "${alt}"`);
|
|
540
552
|
cfg = { ...cfg, space: alt };
|
|
541
553
|
try {
|