@join-pantheon/cli 0.29.1 → 0.31.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.
Files changed (57) hide show
  1. package/README.md +142 -40
  2. package/bin/pantheon.js +12 -0
  3. package/dist/accounts.js +151 -26
  4. package/dist/agents.js +224 -17
  5. package/dist/claude-confine.js +299 -0
  6. package/dist/cli.js +1116 -137
  7. package/dist/client.js +294 -10
  8. package/dist/command-state.js +102 -0
  9. package/dist/config.js +287 -14
  10. package/dist/continued.js +42 -10
  11. package/dist/doctor.js +191 -23
  12. package/dist/file-lock.js +197 -0
  13. package/dist/grants.js +1 -1
  14. package/dist/history-view.js +2 -1
  15. package/dist/home-guard.js +565 -0
  16. package/dist/hooks-install.js +2 -1
  17. package/dist/identity.js +101 -1
  18. package/dist/install.js +76 -26
  19. package/dist/kiro-files.js +2 -2
  20. package/dist/ledger.js +111 -28
  21. package/dist/machine.js +494 -32
  22. package/dist/mcp-supervisor.js +29 -7
  23. package/dist/mcp-windows.js +47 -25
  24. package/dist/mcp.js +99 -36
  25. package/dist/network.js +39 -8
  26. package/dist/nonces.js +130 -0
  27. package/dist/notify.js +26 -9
  28. package/dist/pending.js +121 -22
  29. package/dist/persona.js +21 -0
  30. package/dist/pins.js +161 -0
  31. package/dist/profile.js +22 -1
  32. package/dist/protocol.js +40 -1
  33. package/dist/quickstart.js +173 -24
  34. package/dist/reads.js +37 -0
  35. package/dist/refusal.js +14 -1
  36. package/dist/relay.js +1341 -207
  37. package/dist/repo-find.js +127 -0
  38. package/dist/repo-route.js +70 -0
  39. package/dist/roster-file.js +190 -25
  40. package/dist/run-failure.js +99 -3
  41. package/dist/secret-guard.js +141 -0
  42. package/dist/service.js +91 -35
  43. package/dist/setup-guide.js +91 -31
  44. package/dist/spend.js +6 -2
  45. package/dist/store.js +94 -10
  46. package/dist/text-file.js +74 -0
  47. package/dist/tray.js +24 -3
  48. package/dist/untrusted.js +24 -5
  49. package/dist/update-check.js +2 -0
  50. package/dist/upgrade.js +9 -5
  51. package/dist/verify-act.js +102 -0
  52. package/dist/which.js +69 -12
  53. package/dist/worker-fleet.js +30 -9
  54. package/dist/worker-policy.js +231 -7
  55. package/dist/worker.js +1717 -130
  56. package/helper/macos/menubar.swift +63 -2
  57. package/package.json +2 -2
package/dist/config.js CHANGED
@@ -1,8 +1,10 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
1
+ import { readFileSync, mkdirSync, existsSync, openSync, writeSync, fsyncSync, closeSync, renameSync, rmSync, statSync, chmodSync, realpathSync } from "node:fs";
2
2
  import { execFileSync } from "node:child_process";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomBytes } from "node:crypto";
4
4
  import { homedir } from "node:os";
5
- import { join, resolve } from "node:path";
5
+ import { join, resolve, sep } from "node:path";
6
+ import { VERSION } from "./version.js";
7
+ import { repoName } from "./repo-route.js";
6
8
  export const CONFIG_DIR = process.env.PANTHEON_HOME ?? join(homedir(), ".pantheon");
7
9
  export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
8
10
  export const DEFAULT_RELAY = process.env.PANTHEON_DEFAULT_RELAY
@@ -60,13 +62,34 @@ function sameRepo(host, path) {
60
62
  return [host, path];
61
63
  }
62
64
  const repoCache = new Map();
65
+ const gitUnanswered = new Set();
66
+ export function repoUnknown(dir = process.cwd()) {
67
+ const key = resolve(dir);
68
+ if (!gitUnanswered.has(key))
69
+ return false;
70
+ for (let d = key;; d = resolve(d, "..")) {
71
+ if (existsSync(join(d, ".git")))
72
+ return true;
73
+ if (resolve(d, "..") === d)
74
+ return false;
75
+ }
76
+ }
63
77
  export function repoInfo(dir = process.cwd()) {
64
78
  const key = resolve(dir);
65
79
  if (repoCache.has(key))
66
80
  return repoCache.get(key);
67
81
  let info = null;
68
82
  try {
69
- const root = execFileSync("git", ["-C", key, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 }).trim();
83
+ let root;
84
+ try {
85
+ root = execFileSync("git", ["-C", key, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim();
86
+ }
87
+ catch (e) {
88
+ if (e.status !== 128)
89
+ gitUnanswered.add(key);
90
+ throw e;
91
+ }
92
+ gitUnanswered.delete(key);
70
93
  let origin = null;
71
94
  try {
72
95
  origin = normalizeOrigin(execFileSync("git", ["-C", root, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 }));
@@ -90,7 +113,10 @@ export function migrate(raw) {
90
113
  f.bindings ??= {};
91
114
  if (!raw.version && f.space && f.token && !Object.values(f.spaces).some((m) => m.name === f.space && m.relay === f.relay)) {
92
115
  const id = legacySpaceId(f.relay, f.space);
93
- f.spaces[id] = { id, name: f.space, relay: f.relay, token: f.token, joined_at: Date.now() };
116
+ f.spaces[id] = {
117
+ id, name: f.space, relay: f.relay, token: f.token, joined_at: Date.now(),
118
+ ...(raw.pinTeam ? { pin_team: raw.pinTeam } : {}), ...(raw.joinedWith ? { since: raw.joinedWith } : {}),
119
+ };
94
120
  f.default_space ??= id;
95
121
  }
96
122
  if (f.default_space && !f.spaces[f.default_space])
@@ -103,7 +129,9 @@ export function migrate(raw) {
103
129
  export function loadAll() {
104
130
  if (!existsSync(CONFIG_PATH))
105
131
  return null;
106
- return migrate(JSON.parse(readFileSync(CONFIG_PATH, "utf8")));
132
+ if (!statSync(CONFIG_PATH).isFile())
133
+ throw new Error(`${CONFIG_PATH} is not a plain file — remove it and set up again (pantheon doctor)`);
134
+ return migrate(JSON.parse(readFileSync(CONFIG_PATH, "utf8").replace(/^\uFEFF/, "")));
107
135
  }
108
136
  export function saveAll(f) {
109
137
  const out = { ...f, version: 2, spaces: f.spaces ?? {}, bindings: f.bindings ?? {} };
@@ -114,10 +142,113 @@ export function saveAll(f) {
114
142
  const def = out.default_space ? out.spaces[out.default_space] : undefined;
115
143
  out.space = def?.name ?? "";
116
144
  out.token = def?.token ?? "";
117
- mkdirSync(CONFIG_DIR, { recursive: true });
118
- writeFileSync(CONFIG_PATH, JSON.stringify(out, null, 2) + "\n", { mode: 0o600 });
145
+ writeConfigAtomic(JSON.stringify(out, null, 2) + "\n");
119
146
  return CONFIG_PATH;
120
147
  }
148
+ function privateDir() {
149
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
150
+ if (process.platform === "win32")
151
+ return;
152
+ try {
153
+ if ((statSync(CONFIG_DIR).mode & 0o077) !== 0)
154
+ chmodSync(CONFIG_DIR, 0o700);
155
+ }
156
+ catch { }
157
+ }
158
+ function writeConfigAtomic(data) {
159
+ privateDir();
160
+ const tmp = `${CONFIG_PATH}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
161
+ try {
162
+ const fd = openSync(tmp, "w", 0o600);
163
+ try {
164
+ writeSync(fd, data);
165
+ fsyncSync(fd);
166
+ }
167
+ finally {
168
+ closeSync(fd);
169
+ }
170
+ if (process.platform !== "win32")
171
+ chmodSync(tmp, 0o600);
172
+ for (let i = 0;; i++) {
173
+ try {
174
+ renameSync(tmp, CONFIG_PATH);
175
+ break;
176
+ }
177
+ catch (e) {
178
+ if (i >= 20 || !["EPERM", "EACCES", "EBUSY"].includes(e.code ?? ""))
179
+ throw e;
180
+ sleepSync(10);
181
+ }
182
+ }
183
+ }
184
+ catch (e) {
185
+ try {
186
+ rmSync(tmp, { force: true });
187
+ }
188
+ catch { }
189
+ throw e;
190
+ }
191
+ let dfd = null;
192
+ try {
193
+ dfd = openSync(CONFIG_DIR, "r");
194
+ fsyncSync(dfd);
195
+ }
196
+ catch { }
197
+ finally {
198
+ if (dfd !== null)
199
+ try {
200
+ closeSync(dfd);
201
+ }
202
+ catch { }
203
+ }
204
+ }
205
+ const CONFIG_LOCK_STALE_MS = 5_000;
206
+ const CONFIG_LOCK_WAIT_MS = 15_000;
207
+ const sleepSync = (ms) => { try {
208
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
209
+ }
210
+ catch { } };
211
+ export function withConfigLock(fn) {
212
+ privateDir();
213
+ const lock = `${CONFIG_PATH}.lock`;
214
+ const t0 = Date.now();
215
+ let fd = null;
216
+ while (fd === null) {
217
+ try {
218
+ fd = openSync(lock, "wx", 0o600);
219
+ }
220
+ catch (e) {
221
+ const code = e.code;
222
+ if (code !== "EEXIST" && !(process.platform === "win32" && (code === "EPERM" || code === "EACCES" || code === "EBUSY")))
223
+ throw e;
224
+ try {
225
+ if (Date.now() - statSync(lock).mtimeMs > CONFIG_LOCK_STALE_MS) {
226
+ rmSync(lock, { force: true });
227
+ continue;
228
+ }
229
+ }
230
+ catch {
231
+ continue;
232
+ }
233
+ if (Date.now() - t0 > CONFIG_LOCK_WAIT_MS)
234
+ throw new Error(`config.json is locked by another pantheon process (${lock})`);
235
+ sleepSync(2 + Math.floor(Math.random() * 8));
236
+ }
237
+ }
238
+ try {
239
+ return fn();
240
+ }
241
+ finally {
242
+ try {
243
+ closeSync(fd);
244
+ }
245
+ catch { }
246
+ try {
247
+ rmSync(lock, { force: true });
248
+ }
249
+ catch { }
250
+ }
251
+ }
121
252
  export function configFor(f, m, repo) { return view(f, m, repo); }
122
253
  function view(f, m, repo) {
123
254
  return {
@@ -125,7 +256,10 @@ function view(f, m, repo) {
125
256
  publicKey: f.publicKey, privateKey: f.privateKey,
126
257
  ...(f.display ? { display: f.display } : {}), ...(f.agent ? { agent: f.agent } : {}),
127
258
  ...(f.account ? { account: f.account } : {}), ...(f.pendingAuth ? { pendingAuth: f.pendingAuth } : {}),
128
- ...(m.id.startsWith("legacy:") ? {} : { spaceId: m.id }),
259
+ ...(f.delegate ? { delegate: f.delegate } : {}),
260
+ ...(m.since ? { joinedWith: m.since } : {}),
261
+ ...(m.pin_team ? { pinTeam: m.pin_team } : {}),
262
+ ...(m.id.startsWith("legacy:") ? (f.spaceId ? { spaceId: f.spaceId } : {}) : { spaceId: m.id }),
129
263
  ...(repo ? { repo: repo.fingerprint, ...(repo.origin ? { repoOrigin: repo.origin } : {}) } : {}),
130
264
  };
131
265
  }
@@ -134,15 +268,25 @@ function cwdRepo(repo) {
134
268
  }
135
269
  function envView(onDisk) {
136
270
  const e = process.env;
137
- if (!(e.PANTHEON_RELAY && e.PANTHEON_SPACE && e.PANTHEON_TOKEN && e.PANTHEON_MEMBER))
271
+ if (!(e.PANTHEON_RELAY && e.PANTHEON_SPACE && e.PANTHEON_MEMBER))
272
+ return null;
273
+ const mine = onDisk?.member === e.PANTHEON_MEMBER
274
+ ? Object.values(onDisk.spaces ?? {}).find((m) => (e.PANTHEON_SPACE_ID && m.id === e.PANTHEON_SPACE_ID) || (m.relay === e.PANTHEON_RELAY && m.name === e.PANTHEON_SPACE))
275
+ : undefined;
276
+ const token = e.PANTHEON_TOKEN ?? mine?.token;
277
+ if (!token)
138
278
  return null;
139
279
  const pub = e.PANTHEON_PUBKEY ?? onDisk?.publicKey;
140
280
  const priv = e.PANTHEON_PRIVKEY ?? onDisk?.privateKey;
141
- if (!pub || !priv)
281
+ const delegate = onDisk?.member === e.PANTHEON_MEMBER ? onDisk?.delegate : undefined;
282
+ if (!pub || (!priv && !delegate))
142
283
  return null;
143
284
  return {
144
- relay: e.PANTHEON_RELAY, space: e.PANTHEON_SPACE, token: e.PANTHEON_TOKEN, member: e.PANTHEON_MEMBER,
145
- publicKey: pub, privateKey: priv, display: e.PANTHEON_DISPLAY, agent: e.PANTHEON_AGENT,
285
+ relay: e.PANTHEON_RELAY, space: e.PANTHEON_SPACE, token, member: e.PANTHEON_MEMBER,
286
+ publicKey: pub, privateKey: priv ?? "", display: e.PANTHEON_DISPLAY, agent: e.PANTHEON_AGENT,
287
+ ...(delegate ? { delegate } : {}),
288
+ ...(mine?.pin_team ? { pinTeam: mine.pin_team } : {}),
289
+ ...(mine?.since ? { joinedWith: mine.since } : {}),
146
290
  ...(e.PANTHEON_SPACE_ID ? { spaceId: e.PANTHEON_SPACE_ID } : {}),
147
291
  ...(e.PANTHEON_REPO ? { repo: e.PANTHEON_REPO } : {}),
148
292
  };
@@ -165,7 +309,26 @@ export function resolveFor(cwd = process.cwd(), opts = {}) {
165
309
  return { cfg: applyAgentEnv(view(f, m, repo)), reason: "binding", memberships: n, binding: { ...b, fingerprint: repo.fingerprint }, repo };
166
310
  return { cfg: null, reason: "stale", memberships: n, binding: { ...b, fingerprint: repo.fingerprint }, repo };
167
311
  }
312
+ const owners = f.unbound?.includes(repo.fingerprint) ? [] : Object.values(spaces).filter((m) => m.repos?.some((r) => r.fp === repo.fingerprint));
313
+ if (owners.length === 1) {
314
+ const m = owners[0];
315
+ const binding = { space: m.id, path: repo.root, origin: repo.origin, bound_at: 0, fingerprint: repo.fingerprint };
316
+ return { cfg: applyAgentEnv(view(f, m, repo)), reason: "binding", memberships: n, binding, repo, autoBound: true };
317
+ }
318
+ if (owners.length > 1)
319
+ return { cfg: null, reason: "unbound", memberships: n, repo, claimedBy: owners.map((m) => m.name) };
320
+ }
321
+ if (!repo) {
322
+ const fb = folderBinding(f, cwd);
323
+ if (fb) {
324
+ const m = spaces[fb.space];
325
+ if (m)
326
+ return { cfg: applyAgentEnv(view(f, m, null)), reason: "binding", memberships: n, binding: { ...fb, fingerprint: fb.key }, repo };
327
+ return { cfg: null, reason: "stale", memberships: n, binding: { ...fb, fingerprint: fb.key }, repo };
328
+ }
168
329
  }
330
+ if (!repo && n > 1 && repoUnknown(cwd))
331
+ return { cfg: null, reason: "unbound", memberships: n, repo, repoUnknown: true };
169
332
  if (n === 1) {
170
333
  const m = Object.values(spaces)[0];
171
334
  return { cfg: applyAgentEnv({ ...view(f, m, null), ...cwdRepo(repo) }), reason: "single", memberships: 1, repo };
@@ -255,12 +418,86 @@ export function save(cfg) {
255
418
  }
256
419
  else {
257
420
  const id = cfg.spaceId ?? legacySpaceId(cfg.relay, cfg.space);
258
- f.spaces[id] = { id, name: cfg.space, relay: cfg.relay, token: cfg.token, joined_at: Date.now() };
421
+ f.spaces[id] = { id, name: cfg.space, relay: cfg.relay, token: cfg.token, joined_at: Date.now(), since: VERSION };
259
422
  f.default_space = id;
260
423
  }
261
424
  }
262
425
  return saveAll(f);
263
426
  }
427
+ export function rememberTeamRepos(team, repos) {
428
+ if (!existsSync(CONFIG_PATH))
429
+ return false;
430
+ const next = repos.map((r) => ({ fp: r.fingerprint, name: r.origin ? teamRepoName(r.origin) : r.fingerprint })).sort((a, b) => a.fp.localeCompare(b.fp));
431
+ try {
432
+ return withConfigLock(() => {
433
+ let f;
434
+ try {
435
+ f = loadAll();
436
+ }
437
+ catch {
438
+ return false;
439
+ }
440
+ const m = f && Object.values(f.spaces ?? {}).find((x) => (team.spaceId && x.id === team.spaceId) || (x.name === team.space && x.relay === team.relay));
441
+ if (!f || !m)
442
+ return false;
443
+ if (JSON.stringify(m.repos ?? []) === JSON.stringify(next))
444
+ return false;
445
+ m.repos = next;
446
+ saveAll(f);
447
+ return true;
448
+ });
449
+ }
450
+ catch {
451
+ return false;
452
+ }
453
+ }
454
+ export const teamRepoName = (origin) => repoName(origin);
455
+ export function updateMembershipToken(space, oldToken, newToken) {
456
+ if (!space || !oldToken || !newToken || oldToken === newToken)
457
+ return false;
458
+ if (!existsSync(CONFIG_PATH))
459
+ return false;
460
+ return withConfigLock(() => {
461
+ let f;
462
+ try {
463
+ f = loadAll();
464
+ }
465
+ catch {
466
+ return false;
467
+ }
468
+ if (!f)
469
+ return false;
470
+ let hit = false;
471
+ for (const m of Object.values(f.spaces ?? {})) {
472
+ if (m.name === space && m.token === oldToken) {
473
+ m.token = newToken;
474
+ hit = true;
475
+ }
476
+ }
477
+ if (!hit)
478
+ return false;
479
+ saveAll(f);
480
+ return true;
481
+ });
482
+ }
483
+ export function adoptSpaceId(relay, name, id) {
484
+ if (!/^[A-Za-z0-9_:.-]{1,64}$/.test(id) || !existsSync(CONFIG_PATH))
485
+ return false;
486
+ const f = loadAll();
487
+ if (!f?.spaces)
488
+ return false;
489
+ const legacy = legacySpaceId(relay, name);
490
+ const m = f.spaces[legacy];
491
+ if (!m)
492
+ return false;
493
+ if (f.spaces[id])
494
+ return "conflict";
495
+ delete f.spaces[legacy];
496
+ f.spaces[id] = { ...m, id, pin_team: m.pin_team ?? `${name}@${relay}` };
497
+ adoptId(f, legacy, id);
498
+ saveAll(f);
499
+ return true;
500
+ }
264
501
  function adoptId(f, oldId, newId) {
265
502
  for (const b of Object.values(f.bindings ?? {}))
266
503
  if (b.space === oldId)
@@ -268,6 +505,38 @@ function adoptId(f, oldId, newId) {
268
505
  if (f.default_space === oldId)
269
506
  f.default_space = newId;
270
507
  }
508
+ export function bindFolder(dir, spaceId) {
509
+ const f = loadAll();
510
+ if (!f)
511
+ throw new Error("not configured");
512
+ f.bindings ??= {};
513
+ const path = canonDir(dir);
514
+ const key = `d_${sha16(path)}`;
515
+ const prev = f.bindings[key] ?? null;
516
+ f.bindings[key] = { space: spaceId, path, origin: null, bound_at: Date.now() };
517
+ saveAll(f);
518
+ return { key, replaced: prev && prev.space !== spaceId ? prev : null };
519
+ }
520
+ function canonDir(dir) {
521
+ try {
522
+ return realpathSync.native(dir);
523
+ }
524
+ catch {
525
+ return resolve(dir);
526
+ }
527
+ }
528
+ export function folderBinding(f, dir) {
529
+ const here = canonDir(dir);
530
+ let best = null;
531
+ for (const [key, b] of Object.entries(f.bindings ?? {})) {
532
+ if (!key.startsWith("d_"))
533
+ continue;
534
+ const inside = here === b.path || here.startsWith(b.path.endsWith(sep) ? b.path : b.path + sep);
535
+ if (inside && (!best || b.path.length > best.path.length))
536
+ best = { ...b, key };
537
+ }
538
+ return best;
539
+ }
271
540
  export function bindRepo(repo, spaceId) {
272
541
  const f = loadAll();
273
542
  if (!f)
@@ -275,6 +544,8 @@ export function bindRepo(repo, spaceId) {
275
544
  f.bindings ??= {};
276
545
  const prev = f.bindings[repo.fingerprint] ?? null;
277
546
  f.bindings[repo.fingerprint] = { space: spaceId, path: repo.root, origin: repo.origin, bound_at: Date.now() };
547
+ if (f.unbound?.includes(repo.fingerprint))
548
+ f.unbound = f.unbound.filter((x) => x !== repo.fingerprint);
278
549
  saveAll(f);
279
550
  return { replaced: prev && prev.space !== spaceId ? prev : null };
280
551
  }
@@ -283,6 +554,8 @@ export function unbindRepo(fingerprint) {
283
554
  if (!f?.bindings?.[fingerprint])
284
555
  return false;
285
556
  delete f.bindings[fingerprint];
557
+ if (/^[rl]_/.test(fingerprint))
558
+ f.unbound = [...new Set([...(f.unbound ?? []), fingerprint])].slice(-200);
286
559
  saveAll(f);
287
560
  return true;
288
561
  }
package/dist/continued.js CHANGED
@@ -1,16 +1,18 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { CONFIG_DIR } from "./config.js";
4
+ import { readStateFile, withFileLock } from "./file-lock.js";
5
+ import { writeWhole } from "./pins.js";
4
6
  const KEEP = 500;
5
7
  export function continuedPath(home) {
6
8
  return join(home ?? CONFIG_DIR, "continued.json");
7
9
  }
8
10
  function readAll(home) {
9
11
  try {
10
- const p = continuedPath(home);
11
- if (!existsSync(p))
12
+ const t = readStateFile(continuedPath(home));
13
+ if (t === null)
12
14
  return {};
13
- const v = JSON.parse(readFileSync(p, "utf8"));
15
+ const v = JSON.parse(t);
14
16
  return v && typeof v === "object" ? v : {};
15
17
  }
16
18
  catch {
@@ -20,15 +22,45 @@ function readAll(home) {
20
22
  export function loadContinued(space, home) {
21
23
  return new Set(readAll(home)[space] ?? []);
22
24
  }
25
+ export function handledIn(h) {
26
+ return [...new Set(Object.values(readAll(h)).flatMap((l) => (Array.isArray(l) ? l : [])).filter((x) => typeof x === "string" && x.startsWith("handled:")))];
27
+ }
23
28
  export function markContinued(space, id, home) {
24
29
  try {
25
- const all = readAll(home);
26
- const list = (all[space] ?? []).filter((x) => x !== id);
27
- list.push(id);
28
- all[space] = list.slice(-KEEP);
29
30
  const p = continuedPath(home);
30
- mkdirSync(dirname(p), { recursive: true });
31
- writeFileSync(p, JSON.stringify(all));
31
+ const write = () => {
32
+ const all = readAll(home);
33
+ const list = (all[space] ?? []).filter((x) => x !== id);
34
+ list.push(id);
35
+ all[space] = list.slice(-KEEP);
36
+ mkdirSync(dirname(p), { recursive: true });
37
+ writeWhole(p, JSON.stringify(all));
38
+ };
39
+ try {
40
+ withFileLock(p, write);
41
+ }
42
+ catch {
43
+ write();
44
+ }
45
+ }
46
+ catch { }
47
+ }
48
+ export function unmarkContinued(space, id, home) {
49
+ try {
50
+ const p = continuedPath(home);
51
+ const write = () => {
52
+ const all = readAll(home);
53
+ if (!(all[space] ?? []).includes(id))
54
+ return;
55
+ all[space] = (all[space] ?? []).filter((x) => x !== id);
56
+ writeWhole(p, JSON.stringify(all));
57
+ };
58
+ try {
59
+ withFileLock(p, write);
60
+ }
61
+ catch {
62
+ write();
63
+ }
32
64
  }
33
65
  catch { }
34
66
  }