@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.
- package/README.md +142 -40
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +224 -17
- package/dist/claude-confine.js +299 -0
- package/dist/cli.js +1116 -137
- package/dist/client.js +294 -10
- package/dist/command-state.js +102 -0
- package/dist/config.js +287 -14
- package/dist/continued.js +42 -10
- package/dist/doctor.js +191 -23
- package/dist/file-lock.js +197 -0
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +565 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +101 -1
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +29 -7
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +99 -36
- package/dist/network.js +39 -8
- package/dist/nonces.js +130 -0
- package/dist/notify.js +26 -9
- package/dist/pending.js +121 -22
- package/dist/persona.js +21 -0
- package/dist/pins.js +161 -0
- package/dist/profile.js +22 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +173 -24
- package/dist/reads.js +37 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1341 -207
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +190 -25
- package/dist/run-failure.js +99 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +91 -35
- package/dist/setup-guide.js +91 -31
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +24 -3
- package/dist/untrusted.js +24 -5
- package/dist/update-check.js +2 -0
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/which.js +69 -12
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +231 -7
- package/dist/worker.js +1717 -130
- package/helper/macos/menubar.swift +63 -2
- package/package.json +2 -2
package/dist/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFileSync,
|
|
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
|
-
|
|
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] = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
11
|
-
if (
|
|
12
|
+
const t = readStateFile(continuedPath(home));
|
|
13
|
+
if (t === null)
|
|
12
14
|
return {};
|
|
13
|
-
const v = JSON.parse(
|
|
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
|
-
|
|
31
|
-
|
|
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
|
}
|