parawatch 0.2.1 → 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 +32 -1
- package/dist/bin/cli.js +50 -13
- package/dist/core/doctor.js +33 -10
- package/dist/core/follow.js +36 -49
- package/dist/core/format.js +27 -1
- package/dist/core/integrate.js +3 -4
- package/dist/core/ops.js +118 -45
- package/dist/core/snapshot.js +5 -5
- package/dist/core/state.js +40 -9
- package/dist/core/store.js +204 -24
- package/dist/core/sync.js +317 -0
- package/dist/core/syncthing.js +121 -0
- package/dist/core/types.js +7 -0
- package/package.json +1 -1
- package/skills/parawatch/SKILL.md +4 -0
package/README.md
CHANGED
|
@@ -267,12 +267,43 @@ parawatch watch --from 120 # starting after event 120
|
|
|
267
267
|
|
|
268
268
|
## Storage
|
|
269
269
|
|
|
270
|
-
Everything lives in one append-only file, `~/.local/state/parawatch/events.jsonl` (`$PARAWATCH_HOME`, or `$XDG_STATE_HOME/parawatch`). Writers take a short lock, so several agents can write at once. Next to it: cursors in `cursors/`, running waits in `listeners/`, the last GitHub reading in `github.json`, the last update check in `update.json`, the version each copy (CLI, app) last ran in `clients/`, and the optional `config.json`. Delete the folder to start over.
|
|
270
|
+
Everything lives in one append-only file, `~/.local/state/parawatch/events.jsonl` (`$PARAWATCH_HOME`, or `$XDG_STATE_HOME/parawatch`). Writers take a short lock, so several agents can write at once. Next to it: cursors in `cursors/`, running waits in `listeners/`, the last GitHub reading in `github.json`, the last update check in `update.json`, the version each copy (CLI, app) last ran in `clients/`, and the optional `config.json`. Delete the folder to start over. A shared log lives in the shared folder instead; see [More than one machine](#more-than-one-machine).
|
|
271
271
|
|
|
272
272
|
Every event carries a format number. A copy of parawatch reads a log that a newer copy wrote, skipping what it does not know, but refuses to write to it and says to update. The desktop app's own log is in `~/Library/Logs/Parawatch/main.log` (Help > Show Log File).
|
|
273
273
|
|
|
274
274
|
Desktop notifications for new cards use `osascript` on macOS and `notify-send` on Linux. `PARAWATCH_NOTIFY=0` turns them off. While the desktop app runs it notifies instead, and the CLI stays quiet. To-dos never notify.
|
|
275
275
|
|
|
276
|
+
## More than one machine
|
|
277
|
+
|
|
278
|
+
Agents on a second machine (a desktop PC, a build box) can ask and be answered like the ones on your laptop. Parawatch does not move files between machines itself: point it at a folder that something else keeps in step, and each machine writes only its own log there.
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
# on each machine, once, with its own name
|
|
282
|
+
parawatch sync setup ~/agent-sync/parawatch --device mac --adapter syncthing
|
|
283
|
+
parawatch sync setup ~/agent-sync/parawatch --device pc --adapter syncthing
|
|
284
|
+
parawatch sync # this machine, the others, how the sync tool is doing
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- **Nothing moves until you run `sync setup`.** A machine on its own keeps today's single file and format.
|
|
288
|
+
- **`sync setup` moves this machine's log into the folder.** It becomes `events/<device>.jsonl` with every event, id and seq as it was, so cursors and open cards carry on. The old file is kept beside it as `events.jsonl.before-sync`. A copy of parawatch older than 0.3 on that machine stops writing until it is updated.
|
|
289
|
+
- **Each machine reads every log and writes only its own.** The logs merge in time order. Two machines never write one file, so a sync tool has nothing to merge.
|
|
290
|
+
- **Ids carry the machine once a log is shared:** `pc-c3`, `mac-r2`. Commands take the short form (`parawatch close c3`) while only one machine has that number. Cards from before the move keep their ids.
|
|
291
|
+
- **When two machines answer one card before either has seen the other's answer, the earlier answer stands.** The other one stays on the card, marked as not used, and does not wake the agent. An answer given after seeing the first one counts as usual.
|
|
292
|
+
- **Paths follow the machine.** Attachments, things to open and project folders written on another machine are read as the same place here, under its home folder or the shared folder. Name other folders that sit in different places on each machine (a cloud drive, say): `parawatch sync root gdrive "G:\My Drive"` on one machine and `parawatch sync root gdrive ~/Library/CloudStorage/…/My\ Drive` on the other. Pasted pictures are saved in the shared folder, so every machine can open them.
|
|
293
|
+
- **Waits are seen everywhere.** A card on your laptop says whether an agent on the PC is waiting for it.
|
|
294
|
+
- **Every machine reads GitHub for itself.** The same news is delivered once.
|
|
295
|
+
|
|
296
|
+
The shared folder holds `events/`, `devices/` (each machine's paths, version and last sign of life), `listeners/` and `attachments/`. Cursors, the lock, the GitHub reading and settings stay on each machine.
|
|
297
|
+
|
|
298
|
+
**Sync adapters** are how parawatch asks the sync tool how it is doing, for `parawatch sync`, `doctor` and the app's settings. Pick one with `--adapter` (or `"sync": { "adapter": … }` in `config.json`):
|
|
299
|
+
|
|
300
|
+
| Adapter | Reports |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `syncthing` | Asks the local Syncthing through its REST API (address and API key from its `config.xml`; `PARAWATCH_SYNCTHING_KEY` or `"sync": { "syncthing": { "url", "home" } }` override): connected devices, when the folder was last in step, files on the way, errors. |
|
|
303
|
+
| `folder` | Anything else that keeps a folder in step (a cloud drive, a network share): the other machines' last writes and conflict copies. |
|
|
304
|
+
|
|
305
|
+
An adapter is one object with a `status()` method (`src/core/sync.ts`, `registerAdapter`). Another tool, such as a git checkout or a small server, fits the same shape.
|
|
306
|
+
|
|
276
307
|
## Not goals
|
|
277
308
|
|
|
278
309
|
- Starting or steering agents. Use whatever you already use; parawatch only watches and carries answers back.
|
package/dist/bin/cli.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { UserError, addProject, answerCard, beatRun, claimSession, closeCard, commentCard, consumerName, editCard, endRun, linkPr, listeners, listenersFor, noteRun, openCard, prEvent, readInbox, removeProject, startListening, startRun, unlinkPr, waitInbox, } from "../core/ops.js";
|
|
5
|
+
import { UserError, addProject, answerCard, beatRun, claimSession, closeCard, commentCard, consumerName, editCard, endRun, linkPr, listeners, listenersFor, noteRun, openCard, prEvent, findCard, readInbox, readState, removeProject, startListening, startRun, unlinkPr, waitInbox, } from "../core/ops.js";
|
|
6
6
|
import { githubConfig, readCache, resolvePrRef, syncGithub, prViews } from "../core/github.js";
|
|
7
7
|
import { digest, digestText, localDate } from "../core/digest.js";
|
|
8
|
-
import { readEvents } from "../core/store.js";
|
|
8
|
+
import { everywhere, readEvents } from "../core/store.js";
|
|
9
9
|
import { followEvents } from "../core/follow.js";
|
|
10
|
-
import {
|
|
10
|
+
import { adapterNames, setRoot, setupSync, syncAdapter, syncStatus, touchDevice } from "../core/sync.js";
|
|
11
|
+
import { syncText } from "../core/format.js";
|
|
11
12
|
import { collectRuns } from "../core/runs.js";
|
|
12
13
|
import { askRows, cardDetail, cardLine, cardRows, inboxLine, prRows, runsTree, view } from "../core/format.js";
|
|
13
14
|
import { hookContext, notify, removeInstructions, writeInstructions } from "../core/integrate.js";
|
|
@@ -42,6 +43,7 @@ function binPath() {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
recordClient({ name: "cli", version: VERSION, path: binPath() });
|
|
46
|
+
touchDevice(VERSION);
|
|
45
47
|
const program = new Command();
|
|
46
48
|
program
|
|
47
49
|
.name("parawatch")
|
|
@@ -50,7 +52,7 @@ program
|
|
|
50
52
|
/** Read GitHub when the last reading is old; never fails the command. */
|
|
51
53
|
async function refreshGithub(force = false) {
|
|
52
54
|
try {
|
|
53
|
-
await syncGithub({ state:
|
|
55
|
+
await syncGithub({ state: readState(), force });
|
|
54
56
|
}
|
|
55
57
|
catch (err) {
|
|
56
58
|
if (force)
|
|
@@ -129,7 +131,7 @@ program
|
|
|
129
131
|
.option("--copy", "also put a line about the card on the clipboard, to paste into that session")
|
|
130
132
|
.option("--print", "print the link instead of opening it")
|
|
131
133
|
.action((target, o) => {
|
|
132
|
-
const g = resolveGoto(target,
|
|
134
|
+
const g = resolveGoto(target, readState());
|
|
133
135
|
if (o.copy && g.card) {
|
|
134
136
|
const line = cardContextLine(g.card);
|
|
135
137
|
try {
|
|
@@ -171,7 +173,7 @@ program
|
|
|
171
173
|
.description("show one card with its answers")
|
|
172
174
|
.option("--json", "JSON output")
|
|
173
175
|
.action((id, o) => {
|
|
174
|
-
const card =
|
|
176
|
+
const card = findCard(readState(), id);
|
|
175
177
|
if (!card)
|
|
176
178
|
throw new UserError(`no card "${id}"`);
|
|
177
179
|
if (o.json) {
|
|
@@ -193,7 +195,7 @@ program
|
|
|
193
195
|
.option("-a, --all", "include closed cards")
|
|
194
196
|
.option("--json", "JSON output")
|
|
195
197
|
.action((o) => {
|
|
196
|
-
const cards = [...
|
|
198
|
+
const cards = [...readState().cards.values()].filter((c) => (o.all || c.status !== "closed") && (!o.project || c.project === o.project));
|
|
197
199
|
if (o.json)
|
|
198
200
|
print({ v: SCHEMA_VERSION, cards });
|
|
199
201
|
else
|
|
@@ -239,9 +241,9 @@ inboxOptions(program.command("inbox").description("print unhandled answers witho
|
|
|
239
241
|
program
|
|
240
242
|
.command("watch")
|
|
241
243
|
.description("stream every new event as one JSON line (for plugins and monitors)")
|
|
242
|
-
.option("--from <seq>", "replay events after this sequence number first", int)
|
|
244
|
+
.option("--from <seq>", "replay events after this sequence number first (in every machine's log)", int)
|
|
243
245
|
.action(async (o) => {
|
|
244
|
-
followEvents(print, o.from);
|
|
246
|
+
followEvents(print, o.from === undefined ? undefined : everywhere(o.from, readEvents()));
|
|
245
247
|
await new Promise(() => { });
|
|
246
248
|
});
|
|
247
249
|
// ------------------------------------------------------------------ runs
|
|
@@ -256,7 +258,7 @@ program
|
|
|
256
258
|
.option("-w, --wide", "show runtime and ids")
|
|
257
259
|
.option("--json", "JSON output")
|
|
258
260
|
.action((o) => {
|
|
259
|
-
const runs = collectRuns(
|
|
261
|
+
const runs = collectRuns(readState(), { sinceHours: o.since, project: o.project, includeEnded: o.all, runtimes: o.runtime, keepRepeats: o.every });
|
|
260
262
|
if (o.json)
|
|
261
263
|
print({ v: SCHEMA_VERSION, runs });
|
|
262
264
|
else
|
|
@@ -322,7 +324,7 @@ pr.command("list")
|
|
|
322
324
|
.action(async (o) => {
|
|
323
325
|
if (!o.offline)
|
|
324
326
|
await refreshGithub();
|
|
325
|
-
const state =
|
|
327
|
+
const state = readState();
|
|
326
328
|
const runs = collectRuns(state, { sinceHours: 72 });
|
|
327
329
|
let prs = prViews(readCache(), state, runs);
|
|
328
330
|
if (o.project)
|
|
@@ -335,7 +337,7 @@ pr.command("list")
|
|
|
335
337
|
pr.command("sync")
|
|
336
338
|
.description("read GitHub now")
|
|
337
339
|
.action(async () => {
|
|
338
|
-
const c = await syncGithub({ state:
|
|
340
|
+
const c = await syncGithub({ state: readState(), force: true });
|
|
339
341
|
if (c.error)
|
|
340
342
|
process.stderr.write(`parawatch: ${c.error}\n`);
|
|
341
343
|
process.stdout.write(`${c.prs.filter((p) => p.state === "open").length} open pull requests on ${(c.hosts ?? []).join(", ") || "no host"}\n`);
|
|
@@ -480,7 +482,7 @@ project
|
|
|
480
482
|
.command("list")
|
|
481
483
|
.option("--json", "JSON output")
|
|
482
484
|
.action((o) => {
|
|
483
|
-
const ps = [...
|
|
485
|
+
const ps = [...readState().projects.values()];
|
|
484
486
|
if (o.json)
|
|
485
487
|
print({ v: SCHEMA_VERSION, projects: ps });
|
|
486
488
|
else
|
|
@@ -513,6 +515,41 @@ program
|
|
|
513
515
|
else
|
|
514
516
|
process.stdout.write(`${doctorText(checks, view({}).color)}\n`);
|
|
515
517
|
});
|
|
518
|
+
// ------------------------------------------------------------------ sharing between machines
|
|
519
|
+
const sync = program.command("sync").description("share cards and answers with your other machines through a synced folder");
|
|
520
|
+
sync
|
|
521
|
+
.command("status", { isDefault: true })
|
|
522
|
+
.description("how sharing is doing: this machine, the others, the sync tool")
|
|
523
|
+
.option("--json", "JSON output")
|
|
524
|
+
.action(async (o) => {
|
|
525
|
+
const st = await syncStatus();
|
|
526
|
+
if (o.json)
|
|
527
|
+
print({ v: SCHEMA_VERSION, sync: st ?? null });
|
|
528
|
+
else
|
|
529
|
+
process.stdout.write(syncText(st, view({}).color) + "\n");
|
|
530
|
+
});
|
|
531
|
+
sync
|
|
532
|
+
.command("setup <folder>")
|
|
533
|
+
.description("start writing to a shared folder; this machine's log moves there, nothing is lost")
|
|
534
|
+
.option("-d, --device <name>", "this machine's name in the folder (a-z, 0-9, -); defaults to the host name")
|
|
535
|
+
.option("-a, --adapter <name>", `the tool that carries the folder: ${adapterNames().join(", ")}`)
|
|
536
|
+
.action((folder, o) => {
|
|
537
|
+
const r = setupSync({ dir: folder, device: o.device, adapter: o.adapter, version: VERSION });
|
|
538
|
+
const lines = [
|
|
539
|
+
`This machine writes to ${r.dir} as "${r.device}" (adapter: ${r.adapter}, ${syncAdapter(r.adapter).about}).`,
|
|
540
|
+
r.moved ? `Moved ${r.moved} events from this machine's log; the old file is kept at ${r.backup}.` : "",
|
|
541
|
+
r.others.length ? `Other machines already here: ${r.others.join(", ")}.` : "No other machine has written here yet. Run the same command on the next one with its own --device.",
|
|
542
|
+
"Older parawatch copies (before 0.3) on this machine stop writing until they are updated.",
|
|
543
|
+
];
|
|
544
|
+
process.stdout.write(lines.filter(Boolean).join("\n") + "\n");
|
|
545
|
+
});
|
|
546
|
+
sync
|
|
547
|
+
.command("root <name> [folder]")
|
|
548
|
+
.description("name a folder on this machine (a cloud drive, say) so paths under it open on the others; no folder removes it")
|
|
549
|
+
.action((name, folder) => {
|
|
550
|
+
const d = setRoot(name, folder, VERSION);
|
|
551
|
+
process.stdout.write(Object.entries(d.roots ?? {}).map(([k, v]) => `${k} ${v}`).join("\n") + (d.roots ? "\n" : "No named folders.\n"));
|
|
552
|
+
});
|
|
516
553
|
program
|
|
517
554
|
.command("hook <runtime>")
|
|
518
555
|
.description("hook entry point; 'claude-code' reads the hook JSON on stdin")
|
package/dist/core/doctor.js
CHANGED
|
@@ -10,9 +10,10 @@ import { ghHosts, githubConfig, readCache, runGh } from "./github.js";
|
|
|
10
10
|
import { instructionState } from "./integrate.js";
|
|
11
11
|
import { listeners } from "./ops.js";
|
|
12
12
|
import { fold } from "./state.js";
|
|
13
|
-
import {
|
|
13
|
+
import { deviceName, eventFiles, homeDir, readEvents, sharedDir, storeFormat } from "./store.js";
|
|
14
|
+
import { readDevice, syncStatus } from "./sync.js";
|
|
14
15
|
import { skillState, skillTargets } from "./skill.js";
|
|
15
|
-
import {
|
|
16
|
+
import { STORE_FORMAT } from "./types.js";
|
|
16
17
|
import { updateChecksOn } from "./update.js";
|
|
17
18
|
function claudeSettingsFiles() {
|
|
18
19
|
const dir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
@@ -38,17 +39,39 @@ export async function doctor(input) {
|
|
|
38
39
|
// the store
|
|
39
40
|
const events = readEvents();
|
|
40
41
|
let size = 0;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
for (const f of eventFiles()) {
|
|
43
|
+
try {
|
|
44
|
+
size += fs.statSync(f.file).size;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// no events yet
|
|
48
|
+
}
|
|
46
49
|
}
|
|
47
50
|
const format = storeFormat(events);
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const shared = sharedDir();
|
|
52
|
+
const where = shared ? `${shared} (shared) · this machine ${homeDir()}` : homeDir();
|
|
53
|
+
if (format > STORE_FORMAT)
|
|
54
|
+
add("store", "warn", `${where} has format ${format}, newer than this copy reads (${STORE_FORMAT})`, CLI_UPDATE);
|
|
50
55
|
else
|
|
51
|
-
add("store", "ok", `${
|
|
56
|
+
add("store", "ok", `${where} · ${events.length} events, ${bytes(size)}`);
|
|
57
|
+
// other machines
|
|
58
|
+
const st = input.sync === undefined ? await syncStatus().catch(() => undefined) : input.sync ?? undefined;
|
|
59
|
+
if (!shared)
|
|
60
|
+
add("sync", "off", "this machine only", "to share with another machine: parawatch sync setup <folder> --device <name>");
|
|
61
|
+
else {
|
|
62
|
+
const me = deviceName();
|
|
63
|
+
const mine = readDevice(me);
|
|
64
|
+
if (mine && mine.host !== `${os.hostname()}:${os.homedir()}`)
|
|
65
|
+
add("sync", "warn", `another machine (${mine.host}) also writes as "${me}"`, "give each machine its own name: parawatch sync setup <folder> --device <name>");
|
|
66
|
+
if (!st)
|
|
67
|
+
add("sync", "info", `${me} · ${shared}`);
|
|
68
|
+
else {
|
|
69
|
+
const peers = st.peers.map((p) => `${p.name} ${p.connected === true ? "connected" : p.connected === false ? "not connected" : "seen"}`).join(", ");
|
|
70
|
+
add("sync", st.error || !st.ok ? "warn" : "ok", `${me} · ${st.adapter} · ${peers || "no other machine yet"}${st.error ? ` · ${st.error}` : ""}`);
|
|
71
|
+
if (st.conflicts.length)
|
|
72
|
+
add("sync", "warn", `${st.conflicts.length} conflict copy(ies): ${st.conflicts.slice(0, 3).join(", ")}`, "each machine writes only its own file; look for two machines using one device name");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
52
75
|
const state = fold(events);
|
|
53
76
|
add("projects", state.projects.size ? "ok" : "info", state.projects.size ? [...state.projects.keys()].join(", ") : "none registered (claims still work)");
|
|
54
77
|
// the desktop app
|
package/dist/core/follow.js
CHANGED
|
@@ -1,62 +1,49 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { endOf, eventFiles, eventsDir, isAfter, parseEventLines, readEvents } from "./store.js";
|
|
3
3
|
/**
|
|
4
|
-
* Call `onEvent` for every event appended after `
|
|
5
|
-
* undefined). Watches the
|
|
4
|
+
* Call `onEvent` for every event appended after `after` (or after the current end when it is
|
|
5
|
+
* undefined), in any machine's log. Watches the log folder and also checks once a second, so a
|
|
6
|
+
* log a sync tool brings in from another machine shows up too. Returns a stop function.
|
|
6
7
|
*/
|
|
7
|
-
export function followEvents(onEvent,
|
|
8
|
-
const
|
|
9
|
-
let
|
|
10
|
-
let lastSeq = afterSeq ?? Infinity;
|
|
11
|
-
try {
|
|
12
|
-
const text = fs.readFileSync(file, "utf8");
|
|
13
|
-
offset = Buffer.byteLength(text);
|
|
14
|
-
const events = parseEventLines(text);
|
|
15
|
-
const end = events[events.length - 1]?.seq ?? 0;
|
|
16
|
-
if (afterSeq === undefined)
|
|
17
|
-
lastSeq = end;
|
|
18
|
-
else {
|
|
19
|
-
for (const ev of events)
|
|
20
|
-
if (ev.seq > afterSeq)
|
|
21
|
-
onEvent(ev);
|
|
22
|
-
lastSeq = Math.max(afterSeq, end);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
lastSeq = afterSeq ?? 0;
|
|
27
|
-
}
|
|
8
|
+
export function followEvents(onEvent, after) {
|
|
9
|
+
const offsets = new Map();
|
|
10
|
+
let pos = after ?? endOf(readEvents());
|
|
28
11
|
const pump = () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
offset = 0;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
12
|
+
for (const f of eventFiles()) {
|
|
13
|
+
let size = 0;
|
|
14
|
+
try {
|
|
15
|
+
size = fs.statSync(f.file).size;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
let offset = offsets.get(f.file) ?? 0;
|
|
21
|
+
if (size < offset)
|
|
22
|
+
offset = 0; // file replaced
|
|
23
|
+
if (size === offset)
|
|
24
|
+
continue;
|
|
25
|
+
const fd = fs.openSync(f.file, "r");
|
|
26
|
+
const buf = Buffer.alloc(size - offset);
|
|
27
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
28
|
+
fs.closeSync(fd);
|
|
29
|
+
const text = buf.toString("utf8");
|
|
30
|
+
const cut = text.lastIndexOf("\n");
|
|
31
|
+
if (cut < 0)
|
|
32
|
+
continue;
|
|
33
|
+
offsets.set(f.file, offset + Buffer.byteLength(text.slice(0, cut + 1)));
|
|
34
|
+
for (const ev of parseEventLines(text.slice(0, cut + 1), f.device)) {
|
|
35
|
+
if (!isAfter(ev, pos))
|
|
36
|
+
continue;
|
|
37
|
+
pos = { ...pos, [f.device]: ev.seq };
|
|
51
38
|
onEvent(ev);
|
|
52
|
-
lastSeq = ev.seq;
|
|
53
39
|
}
|
|
54
40
|
}
|
|
55
41
|
};
|
|
42
|
+
pump();
|
|
56
43
|
let watcher;
|
|
57
44
|
try {
|
|
58
|
-
fs.mkdirSync(
|
|
59
|
-
watcher = fs.watch(
|
|
45
|
+
fs.mkdirSync(eventsDir(), { recursive: true });
|
|
46
|
+
watcher = fs.watch(eventsDir(), () => pump());
|
|
60
47
|
}
|
|
61
48
|
catch {
|
|
62
49
|
watcher = undefined; // the timer alone still works
|
package/dist/core/format.js
CHANGED
|
@@ -138,9 +138,13 @@ export function cardDetail(c) {
|
|
|
138
138
|
if (c.from || c.session)
|
|
139
139
|
out.push(`From: ${[c.from, c.session ? `session ${c.session.slice(0, 8)}` : ""].filter(Boolean).join(", ")}`);
|
|
140
140
|
out.push(`Opened: ${new Date(c.openedAt).toLocaleString()}${c.since ? `, first came up ${new Date(c.since).toLocaleDateString()}` : ""}`);
|
|
141
|
+
if (c.dev)
|
|
142
|
+
out.push(`Machine: ${c.dev}`);
|
|
141
143
|
for (const a of c.answers) {
|
|
142
144
|
const choice = a.choice !== undefined ? ` chose ${a.choice}${c.options?.[a.choice - 1] ? ` (${c.options[a.choice - 1]})` : ""}` : "";
|
|
143
|
-
|
|
145
|
+
const where = a.dev ? ` on ${a.dev}` : "";
|
|
146
|
+
const late = a.late ? " (not used: given at the same time as the answer before it, which stands)" : "";
|
|
147
|
+
out.push("", `${a.by}${where}${choice}${a.comment ? `: ${a.comment}` : ""}${late}`);
|
|
144
148
|
for (const f of a.attach ?? [])
|
|
145
149
|
out.push(` attached ${f}`);
|
|
146
150
|
}
|
|
@@ -348,4 +352,26 @@ export function runsTree(runs, v = PLAIN, now = Date.now(), indent = "") {
|
|
|
348
352
|
}
|
|
349
353
|
return out.join("\n");
|
|
350
354
|
}
|
|
355
|
+
// ------------------------------------------------------------------ sharing
|
|
356
|
+
/** `parawatch sync status` as text. */
|
|
357
|
+
export function syncText(st, color = false, now = Date.now()) {
|
|
358
|
+
const v = { ...PLAIN, color };
|
|
359
|
+
if (!st)
|
|
360
|
+
return "This machine keeps its log to itself. To share it with another machine: parawatch sync setup <folder> --device <name>";
|
|
361
|
+
const lines = [
|
|
362
|
+
`${paint(v, st.device, "bold")} writes to ${st.dir}`,
|
|
363
|
+
`adapter ${st.adapter}${st.ok ? "" : paint(v, " (not reachable)", "yellow")}${st.lastSync ? ` · last in step ${ago(st.lastSync, now)} ago` : ""}${st.pending ? ` · ${st.pending} file(s) on the way` : ""}`,
|
|
364
|
+
];
|
|
365
|
+
if (!st.peers.length)
|
|
366
|
+
lines.push("peers none yet");
|
|
367
|
+
for (const p of st.peers) {
|
|
368
|
+
const state = p.connected === true ? paint(v, "connected", "green") : p.connected === false ? paint(v, "not connected", "yellow") : "seen";
|
|
369
|
+
lines.push(`peer ${p.name} ${state}${p.lastSeen && p.connected !== true ? ` · last ${ago(p.lastSeen, now)} ago` : ""}${p.address ? ` · ${p.address}` : ""}${p.version ? ` · parawatch ${p.version}` : ""}`);
|
|
370
|
+
}
|
|
371
|
+
if (st.conflicts.length)
|
|
372
|
+
lines.push(paint(v, `conflicts ${st.conflicts.length}:`, "yellow"), ...st.conflicts.map((c) => ` ${c}`));
|
|
373
|
+
if (st.error)
|
|
374
|
+
lines.push(paint(v, st.error, "yellow"));
|
|
375
|
+
return lines.join("\n");
|
|
376
|
+
}
|
|
351
377
|
//# sourceMappingURL=format.js.map
|
package/dist/core/integrate.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readInbox, inboxFrom } from "./ops.js";
|
|
4
|
+
import { readInbox, inboxFrom, readState } from "./ops.js";
|
|
5
5
|
import { homeDir, readCursor, readEvents } from "./store.js";
|
|
6
|
-
import { fold } from "./state.js";
|
|
7
6
|
import { inboxLine } from "./format.js";
|
|
8
7
|
export const BEGIN = "<!-- parawatch:begin -->";
|
|
9
8
|
export const END = "<!-- parawatch:end -->";
|
|
@@ -97,7 +96,7 @@ export function instructionState(file) {
|
|
|
97
96
|
*/
|
|
98
97
|
export function hookContext(input, project) {
|
|
99
98
|
const events = readEvents();
|
|
100
|
-
const state =
|
|
99
|
+
const state = readState(events);
|
|
101
100
|
let name = project;
|
|
102
101
|
if (!name && input.session_id)
|
|
103
102
|
name = state.claims.find((c) => input.session_id.startsWith(c.session))?.project;
|
|
@@ -108,7 +107,7 @@ export function hookContext(input, project) {
|
|
|
108
107
|
}
|
|
109
108
|
if (!name)
|
|
110
109
|
return undefined;
|
|
111
|
-
const items = inboxFrom(events, readCursor(name), { project: name });
|
|
110
|
+
const items = inboxFrom(events, readCursor(name), { project: name }, state);
|
|
112
111
|
if (!items.length)
|
|
113
112
|
return undefined;
|
|
114
113
|
const cards = items.filter((i) => i.kind !== "pr").length;
|