parawatch 0.2.0 → 0.3.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/README.md +55 -2
- package/dist/bin/cli.js +112 -21
- package/dist/core/clients.js +69 -0
- package/dist/core/doctor.js +165 -0
- package/dist/core/errors.js +4 -0
- package/dist/core/follow.js +36 -49
- package/dist/core/format.js +27 -1
- package/dist/core/integrate.js +41 -4
- package/dist/core/ops.js +119 -46
- package/dist/core/semver.js +22 -0
- package/dist/core/skill.js +56 -0
- package/dist/core/snapshot.js +5 -5
- package/dist/core/state.js +40 -9
- package/dist/core/store.js +215 -22
- package/dist/core/sync.js +317 -0
- package/dist/core/syncthing.js +121 -0
- package/dist/core/types.js +7 -0
- package/dist/core/update.js +87 -0
- package/dist/core/version.js +20 -0
- package/package.json +1 -1
- package/skills/parawatch/SKILL.md +4 -0
package/dist/core/snapshot.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { prViews, keyOf, parsePrRef, readCache, githubConfig } from "./github.js";
|
|
2
|
-
import { linkPr, listeners } from "./ops.js";
|
|
2
|
+
import { linkPr, listeners, readState } from "./ops.js";
|
|
3
3
|
import { collectRuns } from "./runs.js";
|
|
4
|
-
import {
|
|
5
|
-
import { readEvents } from "./store.js";
|
|
4
|
+
import { deviceName, eventFiles, sharedDir } from "./store.js";
|
|
6
5
|
import { SCHEMA_VERSION } from "./types.js";
|
|
7
6
|
/**
|
|
8
7
|
* `closed`: also carry the most recently closed cards, newest first, so a panel can show them
|
|
@@ -11,10 +10,10 @@ import { SCHEMA_VERSION } from "./types.js";
|
|
|
11
10
|
* GitHub is read from the last reading only; `syncGithub` refreshes it.
|
|
12
11
|
*/
|
|
13
12
|
export function snapshot(opts = {}) {
|
|
14
|
-
let state =
|
|
13
|
+
let state = readState();
|
|
15
14
|
const runs = collectRuns(state, { sinceHours: opts.sinceHours ?? 6, project: opts.project, keepRepeats: opts.keepRepeats });
|
|
16
15
|
if (linkCreatedPrs(runs, state))
|
|
17
|
-
state =
|
|
16
|
+
state = readState();
|
|
18
17
|
const all = [...state.cards.values()].filter((c) => !opts.project || c.project === opts.project);
|
|
19
18
|
const cards = [...all.filter((c) => c.status !== "closed"), ...recentlyClosed(all, opts.closed ?? 0)];
|
|
20
19
|
quietAnsweredAsks(runs, all);
|
|
@@ -32,6 +31,7 @@ export function snapshot(opts = {}) {
|
|
|
32
31
|
prs,
|
|
33
32
|
github: { enabled, fetchedAt: cache.fetchedAt, error: cache.error },
|
|
34
33
|
listeners: listeners(),
|
|
34
|
+
devices: sharedDir() ? { me: deviceName(), others: eventFiles().map((f) => f.device).filter((d) => d !== deviceName()) } : undefined,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
/** A pull request a session's log shows it opened belongs to that session, unless linked already. */
|
package/dist/core/state.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
function mapAll(xs, map, from) {
|
|
2
|
+
return xs && map ? xs.map((x) => map(x, from)) : xs;
|
|
3
|
+
}
|
|
4
|
+
/** Rebuild current state from the event log. `map` makes other machines' paths local. */
|
|
5
|
+
export function fold(events, map) {
|
|
3
6
|
const s = { projects: new Map(), cards: new Map(), runs: new Map(), claims: [], prLinks: new Map(), lastSeq: 0 };
|
|
4
7
|
for (const ev of events) {
|
|
5
8
|
s.lastSeq = Math.max(s.lastSeq, ev.seq);
|
|
6
9
|
switch (ev.type) {
|
|
7
10
|
case "project.add":
|
|
8
|
-
s.projects.set(ev.project.name, ev.project);
|
|
11
|
+
s.projects.set(ev.project.name, { ...ev.project, paths: mapAll(ev.project.paths, map, ev.dev) ?? [] });
|
|
9
12
|
break;
|
|
10
13
|
case "project.remove":
|
|
11
14
|
s.projects.delete(ev.name);
|
|
12
15
|
break;
|
|
13
|
-
case "card.open":
|
|
14
|
-
|
|
16
|
+
case "card.open": {
|
|
17
|
+
const card = { ...ev.card, openedAt: ev.ts, status: "open", answers: [], comments: [] };
|
|
18
|
+
if (ev.v >= 2 && ev.dev)
|
|
19
|
+
card.dev = ev.dev;
|
|
20
|
+
card.attach = mapAll(card.attach, map, ev.dev);
|
|
21
|
+
card.open = mapAll(card.open, map, ev.dev);
|
|
22
|
+
s.cards.set(ev.card.id, card);
|
|
15
23
|
break;
|
|
24
|
+
}
|
|
16
25
|
case "card.answer": {
|
|
17
26
|
const c = s.cards.get(ev.card);
|
|
18
27
|
if (!c)
|
|
19
28
|
break;
|
|
20
|
-
|
|
29
|
+
const a = { seq: ev.seq, at: ev.ts, by: ev.by, choice: ev.choice, comment: ev.comment, attach: mapAll(ev.attach, map, ev.dev) };
|
|
30
|
+
if (ev.v >= 2 && ev.dev)
|
|
31
|
+
a.dev = ev.dev;
|
|
32
|
+
// written on another machine before that machine saw an earlier answer: the earlier one stands
|
|
33
|
+
if (typeof ev.seen === "number" && ev.seen < c.answers.length)
|
|
34
|
+
a.late = true;
|
|
35
|
+
c.answers.push(a);
|
|
21
36
|
if (c.status === "open")
|
|
22
37
|
c.status = "answered";
|
|
23
38
|
break;
|
|
@@ -93,15 +108,31 @@ export function fold(events) {
|
|
|
93
108
|
}
|
|
94
109
|
return s;
|
|
95
110
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Next id with a prefix, counting the events that created that kind of thing. In a shared folder
|
|
113
|
+
* (`device` given) only that machine's own log counts and the id carries the machine's name, so
|
|
114
|
+
* two machines never pick the same id: `pc-c3`.
|
|
115
|
+
*/
|
|
116
|
+
export function nextId(events, prefix, device) {
|
|
98
117
|
let n = 0;
|
|
99
118
|
for (const ev of events) {
|
|
119
|
+
if (device && ev.dev !== device)
|
|
120
|
+
continue;
|
|
100
121
|
if (prefix === "c" && ev.type === "card.open")
|
|
101
122
|
n++;
|
|
102
123
|
if (prefix === "r" && ev.type === "run.start")
|
|
103
124
|
n++;
|
|
104
125
|
}
|
|
105
|
-
return `${prefix}${n + 1}`;
|
|
126
|
+
return device ? `${device}-${prefix}${n + 1}` : `${prefix}${n + 1}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Find a card or run by id. A shared id may be given without its machine (`c3` for `pc-c3`) as
|
|
130
|
+
* long as only one machine has it; an exact id always wins.
|
|
131
|
+
*/
|
|
132
|
+
export function findId(map, id) {
|
|
133
|
+
if (map.has(id))
|
|
134
|
+
return id;
|
|
135
|
+
const hits = [...map.keys()].filter((k) => k.endsWith(`-${id}`));
|
|
136
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
106
137
|
}
|
|
107
138
|
//# sourceMappingURL=state.js.map
|
package/dist/core/store.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { UserError } from "./errors.js";
|
|
5
|
+
import { STORE_FORMAT } from "./types.js";
|
|
6
|
+
/** Directory holding all parawatch state for this user on this machine. */
|
|
6
7
|
export function homeDir() {
|
|
7
8
|
const explicit = process.env.PARAWATCH_HOME;
|
|
8
9
|
if (explicit)
|
|
@@ -10,20 +11,109 @@ export function homeDir() {
|
|
|
10
11
|
const xdg = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
11
12
|
return path.join(xdg, "parawatch");
|
|
12
13
|
}
|
|
13
|
-
export function
|
|
14
|
-
return path.join(homeDir(), "
|
|
14
|
+
export function configPath() {
|
|
15
|
+
return path.join(homeDir(), "config.json");
|
|
16
|
+
}
|
|
17
|
+
export function readConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = JSON.parse(fs.readFileSync(configPath(), "utf8"));
|
|
20
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
15
25
|
}
|
|
16
|
-
function
|
|
26
|
+
export function writeConfig(c) {
|
|
17
27
|
fs.mkdirSync(homeDir(), { recursive: true });
|
|
28
|
+
const tmp = `${configPath()}.${process.pid}.tmp`;
|
|
29
|
+
fs.writeFileSync(tmp, JSON.stringify(c, null, 2) + "\n");
|
|
30
|
+
fs.renameSync(tmp, configPath());
|
|
31
|
+
}
|
|
32
|
+
export function expandHome(p) {
|
|
33
|
+
return p === "~" ? os.homedir() : p.startsWith("~/") || p.startsWith("~\\") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
34
|
+
}
|
|
35
|
+
export const DEVICE_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
36
|
+
/** A device name made from the host name: lower case, letters, digits and dashes. */
|
|
37
|
+
export function defaultDevice(host = os.hostname()) {
|
|
38
|
+
const short = host.split(".")[0] ?? "";
|
|
39
|
+
const clean = short.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
40
|
+
return DEVICE_NAME.test(clean) ? clean : "this";
|
|
41
|
+
}
|
|
42
|
+
/** The name this machine writes under. */
|
|
43
|
+
export function deviceName() {
|
|
44
|
+
const env = process.env.PARAWATCH_DEVICE?.trim();
|
|
45
|
+
if (env)
|
|
46
|
+
return env;
|
|
47
|
+
const c = readConfig().device;
|
|
48
|
+
return typeof c === "string" && DEVICE_NAME.test(c) ? c : defaultDevice();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The shared folder this machine writes its own log into, when set. Something outside parawatch
|
|
52
|
+
* (Syncthing, a cloud folder) carries that folder between machines.
|
|
53
|
+
*/
|
|
54
|
+
export function sharedDir() {
|
|
55
|
+
const env = process.env.PARAWATCH_SHARED?.trim();
|
|
56
|
+
if (env)
|
|
57
|
+
return path.resolve(expandHome(env));
|
|
58
|
+
const d = readConfig().sync?.dir;
|
|
59
|
+
return typeof d === "string" && d.trim() ? path.resolve(expandHome(d.trim())) : undefined;
|
|
60
|
+
}
|
|
61
|
+
// ------------------------------------------------------------------ files
|
|
62
|
+
/** Where the event logs live: the home alone, or `events/` in the shared folder. */
|
|
63
|
+
export function eventsDir() {
|
|
64
|
+
const shared = sharedDir();
|
|
65
|
+
return shared ? path.join(shared, "events") : homeDir();
|
|
66
|
+
}
|
|
67
|
+
/** The one file this machine appends to. */
|
|
68
|
+
export function eventsPath() {
|
|
69
|
+
const shared = sharedDir();
|
|
70
|
+
return shared ? path.join(shared, "events", `${deviceName()}.jsonl`) : path.join(homeDir(), "events.jsonl");
|
|
71
|
+
}
|
|
72
|
+
/** A device log: `<device>.jsonl`. Sync tools' temp and conflict copies never match. */
|
|
73
|
+
const DEVICE_FILE = /^([a-z0-9][a-z0-9-]{0,31})\.jsonl$/;
|
|
74
|
+
/** Every log to read: one per device in a shared folder, or the single local file. */
|
|
75
|
+
export function eventFiles() {
|
|
76
|
+
const shared = sharedDir();
|
|
77
|
+
if (!shared)
|
|
78
|
+
return [{ device: deviceName(), file: eventsPath() }];
|
|
79
|
+
const dir = path.join(shared, "events");
|
|
80
|
+
const me = deviceName();
|
|
81
|
+
const out = [];
|
|
82
|
+
let names = [];
|
|
83
|
+
try {
|
|
84
|
+
names = fs.readdirSync(dir);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// nothing shared yet
|
|
88
|
+
}
|
|
89
|
+
for (const n of names.sort()) {
|
|
90
|
+
const m = DEVICE_FILE.exec(n);
|
|
91
|
+
if (m)
|
|
92
|
+
out.push({ device: m[1], file: path.join(dir, n) });
|
|
93
|
+
}
|
|
94
|
+
if (!out.some((f) => f.device === me))
|
|
95
|
+
out.push({ device: me, file: eventsPath() });
|
|
96
|
+
return out.sort((a, b) => a.device.localeCompare(b.device));
|
|
18
97
|
}
|
|
98
|
+
/** Where pasted attachments go: inside the shared folder when there is one, so every machine has them. */
|
|
99
|
+
export function attachmentsDir() {
|
|
100
|
+
return path.join(sharedDir() ?? homeDir(), "attachments");
|
|
101
|
+
}
|
|
102
|
+
function ensureDir(dir) {
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
// ------------------------------------------------------------------ lock
|
|
19
106
|
const LOCK_STALE_MS = 30_000;
|
|
20
107
|
const LOCK_WAIT_MS = 10_000;
|
|
21
108
|
function sleepSync(ms) {
|
|
22
109
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
23
110
|
}
|
|
24
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Run fn while holding an exclusive lock file. Several agents on this machine may write at once.
|
|
113
|
+
* The lock is local: each machine only ever writes its own log, so machines never share one.
|
|
114
|
+
*/
|
|
25
115
|
export function withLock(fn) {
|
|
26
|
-
|
|
116
|
+
ensureDir(homeDir());
|
|
27
117
|
const lock = path.join(homeDir(), "lock");
|
|
28
118
|
const started = Date.now();
|
|
29
119
|
let fd;
|
|
@@ -59,28 +149,64 @@ export function withLock(fn) {
|
|
|
59
149
|
fs.rmSync(lock, { force: true });
|
|
60
150
|
}
|
|
61
151
|
}
|
|
62
|
-
|
|
63
|
-
|
|
152
|
+
// ------------------------------------------------------------------ reading
|
|
153
|
+
function readFileEvents(f) {
|
|
64
154
|
let text;
|
|
65
155
|
try {
|
|
66
|
-
text = fs.readFileSync(
|
|
156
|
+
text = fs.readFileSync(f.file, "utf8");
|
|
67
157
|
}
|
|
68
158
|
catch (err) {
|
|
69
159
|
if (err.code === "ENOENT")
|
|
70
160
|
return [];
|
|
71
161
|
throw err;
|
|
72
162
|
}
|
|
73
|
-
return parseEventLines(text);
|
|
163
|
+
return parseEventLines(text, f.device);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Every event, from every machine, in time order. Each machine's own order is kept as written;
|
|
167
|
+
* between machines the clock decides, then the device name, so every machine sees the same order.
|
|
168
|
+
*/
|
|
169
|
+
export function readEvents() {
|
|
170
|
+
const lists = eventFiles().map(readFileEvents).filter((l) => l.length);
|
|
171
|
+
if (lists.length <= 1)
|
|
172
|
+
return lists[0] ?? [];
|
|
173
|
+
return mergeLogs(lists);
|
|
174
|
+
}
|
|
175
|
+
/** Merge per-device logs, each already in its own order, into one. */
|
|
176
|
+
export function mergeLogs(lists) {
|
|
177
|
+
const at = lists.map(() => 0);
|
|
178
|
+
const out = [];
|
|
179
|
+
for (;;) {
|
|
180
|
+
let pick = -1;
|
|
181
|
+
for (let i = 0; i < lists.length; i++) {
|
|
182
|
+
const ev = lists[i][at[i]];
|
|
183
|
+
if (!ev)
|
|
184
|
+
continue;
|
|
185
|
+
if (pick < 0) {
|
|
186
|
+
pick = i;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const best = lists[pick][at[pick]];
|
|
190
|
+
if (ev.ts < best.ts || (ev.ts === best.ts && (ev.dev ?? "") < (best.dev ?? "")))
|
|
191
|
+
pick = i;
|
|
192
|
+
}
|
|
193
|
+
if (pick < 0)
|
|
194
|
+
return out;
|
|
195
|
+
out.push(lists[pick][at[pick]++]);
|
|
196
|
+
}
|
|
74
197
|
}
|
|
75
|
-
export function parseEventLines(text) {
|
|
198
|
+
export function parseEventLines(text, device) {
|
|
76
199
|
const out = [];
|
|
77
200
|
for (const line of text.split("\n")) {
|
|
78
201
|
if (!line.trim())
|
|
79
202
|
continue;
|
|
80
203
|
try {
|
|
81
204
|
const ev = JSON.parse(line);
|
|
82
|
-
if (typeof ev.seq
|
|
83
|
-
|
|
205
|
+
if (typeof ev.seq !== "number" || typeof ev.type !== "string")
|
|
206
|
+
continue;
|
|
207
|
+
if (device)
|
|
208
|
+
ev.dev = device;
|
|
209
|
+
out.push(ev);
|
|
84
210
|
}
|
|
85
211
|
catch {
|
|
86
212
|
// a torn or hand-edited line; keep going
|
|
@@ -88,40 +214,107 @@ export function parseEventLines(text) {
|
|
|
88
214
|
}
|
|
89
215
|
return out;
|
|
90
216
|
}
|
|
217
|
+
// ------------------------------------------------------------------ writing
|
|
91
218
|
/** Append one event. `build` sees the current events so it can pick ids under the lock. */
|
|
92
219
|
export function appendEvent(build) {
|
|
93
220
|
return withLock(() => appendLocked((events) => [build(events)])[0]);
|
|
94
221
|
}
|
|
222
|
+
/** The newest event format in the log. A reader may skip what it does not know; a writer may not. */
|
|
223
|
+
export function storeFormat(events) {
|
|
224
|
+
let v = 0;
|
|
225
|
+
for (const e of events)
|
|
226
|
+
if (typeof e.v === "number" && e.v > v)
|
|
227
|
+
v = e.v;
|
|
228
|
+
return v;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The format this machine writes. A log kept on one machine stays at format 1, which every
|
|
232
|
+
* earlier parawatch reads and writes. A shared folder writes format 2.
|
|
233
|
+
*/
|
|
234
|
+
export function writeFormat() {
|
|
235
|
+
return sharedDir() ? 2 : 1;
|
|
236
|
+
}
|
|
95
237
|
/** Append events while already holding the lock (inside `withLock`). */
|
|
96
238
|
export function appendLocked(build) {
|
|
97
239
|
const events = readEvents();
|
|
98
|
-
|
|
240
|
+
const format = storeFormat(events);
|
|
241
|
+
if (format > STORE_FORMAT) {
|
|
242
|
+
throw new UserError(`${eventsDir()} was written by a newer parawatch (format ${format}; this one knows ${STORE_FORMAT}). Update this copy before it writes: npm i -g parawatch@latest, or update the app.`);
|
|
243
|
+
}
|
|
244
|
+
const me = deviceName();
|
|
245
|
+
const own = sharedDir() ? events.filter((e) => e.dev === me) : events;
|
|
246
|
+
let seq = 0;
|
|
247
|
+
for (const e of own)
|
|
248
|
+
if (e.seq > seq)
|
|
249
|
+
seq = e.seq;
|
|
99
250
|
const ts = new Date().toISOString();
|
|
100
|
-
const
|
|
101
|
-
|
|
251
|
+
const v = writeFormat();
|
|
252
|
+
const out = build(events).map((body) => ({ v, seq: ++seq, ts, ...body }));
|
|
253
|
+
if (out.length) {
|
|
254
|
+
ensureDir(path.dirname(eventsPath()));
|
|
102
255
|
fs.appendFileSync(eventsPath(), out.map((e) => JSON.stringify(e) + "\n").join(""));
|
|
256
|
+
for (const e of out)
|
|
257
|
+
e.dev = me;
|
|
258
|
+
}
|
|
103
259
|
return out;
|
|
104
260
|
}
|
|
261
|
+
/** Whether an event comes after a position. */
|
|
262
|
+
export function isAfter(ev, pos) {
|
|
263
|
+
return ev.seq > (pos[ev.dev ?? deviceName()] ?? 0);
|
|
264
|
+
}
|
|
265
|
+
/** A position moved past these events. */
|
|
266
|
+
export function advance(pos, events) {
|
|
267
|
+
const next = { ...pos };
|
|
268
|
+
const me = deviceName();
|
|
269
|
+
for (const e of events) {
|
|
270
|
+
const d = e.dev ?? me;
|
|
271
|
+
if (e.seq > (next[d] ?? 0))
|
|
272
|
+
next[d] = e.seq;
|
|
273
|
+
}
|
|
274
|
+
return next;
|
|
275
|
+
}
|
|
276
|
+
/** The position at the end of these events. */
|
|
277
|
+
export function endOf(events) {
|
|
278
|
+
return advance({}, events);
|
|
279
|
+
}
|
|
280
|
+
/** A number given on the command line: the same seq in every device's log. */
|
|
281
|
+
export function everywhere(seq, events) {
|
|
282
|
+
const pos = { [deviceName()]: seq };
|
|
283
|
+
for (const e of events)
|
|
284
|
+
if (e.dev)
|
|
285
|
+
pos[e.dev] = seq;
|
|
286
|
+
return pos;
|
|
287
|
+
}
|
|
105
288
|
const CURSOR_NAME = /^[A-Za-z0-9._-]+$/;
|
|
106
289
|
function cursorPath(name) {
|
|
107
290
|
if (!CURSOR_NAME.test(name))
|
|
108
291
|
throw new Error(`parawatch: bad consumer name "${name}"`);
|
|
109
292
|
return path.join(homeDir(), "cursors", `${name}.json`);
|
|
110
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* A cursor file holds `seq` (this machine's log, as every version wrote it) and `pos` (every
|
|
296
|
+
* device). Cursors stay on the machine: two machines never share a reader.
|
|
297
|
+
*/
|
|
111
298
|
export function readCursor(name) {
|
|
112
299
|
try {
|
|
113
300
|
const data = JSON.parse(fs.readFileSync(cursorPath(name), "utf8"));
|
|
114
|
-
|
|
301
|
+
const me = deviceName();
|
|
302
|
+
const pos = data.pos && typeof data.pos === "object" && sharedDir() ? { ...data.pos } : {};
|
|
303
|
+
if (typeof data.seq === "number" && pos[me] === undefined)
|
|
304
|
+
pos[me] = data.seq;
|
|
305
|
+
return pos;
|
|
115
306
|
}
|
|
116
307
|
catch {
|
|
117
|
-
return
|
|
308
|
+
return {};
|
|
118
309
|
}
|
|
119
310
|
}
|
|
120
|
-
export function writeCursor(name,
|
|
311
|
+
export function writeCursor(name, pos) {
|
|
121
312
|
const file = cursorPath(name);
|
|
122
|
-
|
|
313
|
+
ensureDir(path.dirname(file));
|
|
123
314
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
124
|
-
|
|
315
|
+
const seq = pos[deviceName()] ?? 0;
|
|
316
|
+
const body = sharedDir() ? { seq, pos, at: new Date().toISOString() } : { seq, at: new Date().toISOString() };
|
|
317
|
+
fs.writeFileSync(tmp, JSON.stringify(body) + "\n");
|
|
125
318
|
fs.renameSync(tmp, file);
|
|
126
319
|
}
|
|
127
320
|
//# sourceMappingURL=store.js.map
|