passalong 0.16.0 → 0.18.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 +13 -15
- package/bin/passalong +54 -140
- package/package.json +1 -1
- package/skill/SKILL.md +7 -7
- package/src/api.js +43 -4
- package/src/guide.js +2 -1
- package/src/mcp.js +245 -49
- package/src/passalong.js +116 -182
package/src/passalong.js
CHANGED
|
@@ -78,26 +78,19 @@ export async function share(markdown, { cwd = process.cwd(), to, follows } = {})
|
|
|
78
78
|
];
|
|
79
79
|
if (errors.length)
|
|
80
80
|
throw new PassalongError(`guide is not ready to share:\n - ${errors.join("\n - ")}`);
|
|
81
|
-
//
|
|
82
|
-
// says it is ready
|
|
83
|
-
// through here too, so publishing on share would let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
notified = Boolean(res.notified);
|
|
95
|
-
guide = { ...guide, meta: { ...guide.meta, url } };
|
|
96
|
-
} else if (guide.meta.team) {
|
|
97
|
-
throw new PassalongError("sharing to a team needs sync — run `passalong login` first");
|
|
98
|
-
}
|
|
99
|
-
const path = store.save(guide);
|
|
100
|
-
return { guide, path, url, synced, notified };
|
|
81
|
+
// Every guide is a draft when it is first shared, whatever its kind: Draft is where it waits
|
|
82
|
+
// until a person says it is ready — `ready()` — and an agent that writes one over MCP comes
|
|
83
|
+
// through here too, so publishing on share would let it reach people with nobody reading it.
|
|
84
|
+
// Sharing *is* putting it in Draft, so a new guide that names no status says so here; one already
|
|
85
|
+
// published keeps what its document says.
|
|
86
|
+
if (!guide.meta.status) guide = { ...guide, meta: { ...guide.meta, status: "draft" } };
|
|
87
|
+
|
|
88
|
+
// The server is where a guide lives; nothing is kept on this machine.
|
|
89
|
+
needsSync("sharing a guide");
|
|
90
|
+
const res = await api.publish(guide.meta.id, serialize(guide));
|
|
91
|
+
const url = res.url;
|
|
92
|
+
guide = { ...guide, meta: { ...guide.meta, url } };
|
|
93
|
+
return { guide, url, synced: true, notified: Boolean(res.notified) };
|
|
101
94
|
}
|
|
102
95
|
|
|
103
96
|
/**
|
|
@@ -151,8 +144,11 @@ export async function fileBugs(
|
|
|
151
144
|
return { report: { id: report.id, title, environment, team }, issues: filed };
|
|
152
145
|
}
|
|
153
146
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a reference (id, share URL, or local file path) to a guide. An id is the server's to
|
|
149
|
+
* answer: nothing keeps a copy on this machine. `peek` reads without recording a pull.
|
|
150
|
+
*/
|
|
151
|
+
export async function resolve(ref, { peek = false } = {}) {
|
|
156
152
|
if (/^https?:\/\//.test(ref)) {
|
|
157
153
|
const md = await api.fetchShared(ref);
|
|
158
154
|
return { guide: parse(md), from: "link" };
|
|
@@ -162,21 +158,13 @@ export async function resolve(ref) {
|
|
|
162
158
|
}
|
|
163
159
|
if (!ID_RE.test(ref))
|
|
164
160
|
throw new PassalongError(`"${ref}" is not a passalong id, share link, or .md file`);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
store.save(guide); // cache so the next pull is offline
|
|
172
|
-
return { guide, from: "sync" };
|
|
173
|
-
} catch (err) {
|
|
174
|
-
if (err.status !== 404) throw err;
|
|
175
|
-
}
|
|
161
|
+
needsSync("reading a guide by id");
|
|
162
|
+
try {
|
|
163
|
+
return { guide: parse(await api.get(ref, { peek })), from: "sync" };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.status === 404) throw new PassalongError(`no guide "${ref}"`);
|
|
166
|
+
throw err;
|
|
176
167
|
}
|
|
177
|
-
throw new PassalongError(
|
|
178
|
-
`no guide "${ref}" (not local${api.loggedIn() ? " or synced" : "; not logged in, so sync was not checked"})`,
|
|
179
|
-
);
|
|
180
168
|
}
|
|
181
169
|
|
|
182
170
|
/**
|
|
@@ -201,22 +189,21 @@ export async function resolveId(ref) {
|
|
|
201
189
|
}
|
|
202
190
|
|
|
203
191
|
/**
|
|
204
|
-
* Pull a guide
|
|
205
|
-
*
|
|
206
|
-
*
|
|
192
|
+
* Pull a guide and return it, so the caller can put the text straight into an agent's context. A
|
|
193
|
+
* pull of someone else's guide is recorded server-side, which is how the sender sees the transfer
|
|
194
|
+
* landed. Nothing is written unless `save` asks: then it lands in `.passalong/<id>.md` here.
|
|
207
195
|
*/
|
|
208
|
-
export async function pull(ref, { cwd = process.cwd(),
|
|
196
|
+
export async function pull(ref, { cwd = process.cwd(), save = false } = {}) {
|
|
209
197
|
const { guide, from } = await resolve(ref);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return { guide, from, path, markdown: serialize(guide) };
|
|
198
|
+
const markdown = serialize(guide);
|
|
199
|
+
return { guide, from, path: save ? saveTo(cwd, guide.meta.id, markdown) : null, markdown };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Write a guide into `.passalong/<id>.md` under `cwd`, when somebody asked for a file. */
|
|
203
|
+
function saveTo(cwd, id, markdown) {
|
|
204
|
+
const path = join(localDir(cwd), `${id}.md`);
|
|
205
|
+
writeFileSync(path, markdown);
|
|
206
|
+
return path;
|
|
220
207
|
}
|
|
221
208
|
|
|
222
209
|
/**
|
|
@@ -284,17 +271,13 @@ export async function tasks() {
|
|
|
284
271
|
|
|
285
272
|
/**
|
|
286
273
|
* Take the next task for this worktree's repo, or get back the one this worktree already holds.
|
|
287
|
-
* `null` when there is nothing to do.
|
|
274
|
+
* `null` when there is nothing to do. Written to `.passalong/<id>.md` only when `save` asks.
|
|
288
275
|
*/
|
|
289
|
-
export async function nextTask({ cwd = process.cwd(), any = false } = {}) {
|
|
276
|
+
export async function nextTask({ cwd = process.cwd(), any = false, save = false } = {}) {
|
|
290
277
|
needsSync("taking a task");
|
|
291
278
|
const { task } = await api.nextTask({ ...agent(cwd), any });
|
|
292
279
|
if (!task) return null;
|
|
293
|
-
|
|
294
|
-
const path = join(localDir(agent(cwd).worktree), `${task.id}.md`);
|
|
295
|
-
writeFileSync(path, task.markdown);
|
|
296
|
-
store.save(guide);
|
|
297
|
-
return { ...task, path };
|
|
280
|
+
return { ...task, path: save ? saveTo(agent(cwd).worktree, task.id, task.markdown) : null };
|
|
298
281
|
}
|
|
299
282
|
|
|
300
283
|
/** Word from the agent holding a task: renews its lease, and `note` becomes the board's line. */
|
|
@@ -397,18 +380,15 @@ export async function finishTask(
|
|
|
397
380
|
* `next`, the calls that make sense from here, and `say` when the move is to stop.
|
|
398
381
|
*
|
|
399
382
|
* take: this worktree's agent takes guide `id`, or with no id the next one waiting for it. The
|
|
400
|
-
* document is written to `.passalong/<id>.md
|
|
383
|
+
* document comes back in the answer, and is written to `.passalong/<id>.md` only when `save` asks.
|
|
401
384
|
*/
|
|
402
|
-
export async function take(id, { cwd = process.cwd(), any = false } = {}) {
|
|
385
|
+
export async function take(id, { cwd = process.cwd(), any = false, save = false } = {}) {
|
|
403
386
|
needsSync("taking work");
|
|
404
387
|
const who = agent(cwd);
|
|
405
388
|
const r = await api.take({ ...who, ...(id ? { id: await resolveId(id) } : {}), any });
|
|
406
389
|
if (!r.guide?.markdown) return r;
|
|
407
390
|
rememberFence(r.guide.id, r.guide.fence, cwd);
|
|
408
|
-
|
|
409
|
-
writeFileSync(path, r.guide.markdown);
|
|
410
|
-
store.save(parse(r.guide.markdown));
|
|
411
|
-
return { ...r, path };
|
|
391
|
+
return { ...r, path: save ? saveTo(who.worktree, r.guide.id, r.guide.markdown) : null };
|
|
412
392
|
}
|
|
413
393
|
|
|
414
394
|
/** Word from the agent that holds it: the lease starts again, and `note` is what the hub shows. */
|
|
@@ -613,16 +593,19 @@ export async function now({ cwd = process.cwd() } = {}) {
|
|
|
613
593
|
* line the next reader needs.
|
|
614
594
|
*/
|
|
615
595
|
export async function approveTask(id) {
|
|
616
|
-
needsSync("approving
|
|
617
|
-
const
|
|
596
|
+
needsSync("approving work");
|
|
597
|
+
const { guide } = await resolve(id, { peek: true });
|
|
598
|
+
const r = await (guide.meta.kind === "task" ? api.approveTask(id) : api.closeGuide(id));
|
|
618
599
|
store.remove(id);
|
|
619
600
|
return r;
|
|
620
601
|
}
|
|
621
602
|
|
|
622
603
|
export async function rejectTask(id, why) {
|
|
623
|
-
needsSync("
|
|
624
|
-
if (!String(why || "").trim())
|
|
625
|
-
|
|
604
|
+
needsSync("sending work back");
|
|
605
|
+
if (!String(why || "").trim())
|
|
606
|
+
throw new PassalongError("say why: whoever takes it next reads it");
|
|
607
|
+
const { guide } = await resolve(id, { peek: true });
|
|
608
|
+
const r = await (guide.meta.kind === "task" ? api.rejectTask(id, why) : api.sendBack(id, why));
|
|
626
609
|
store.remove(id);
|
|
627
610
|
return r;
|
|
628
611
|
}
|
|
@@ -662,17 +645,10 @@ export async function release(id) {
|
|
|
662
645
|
* - anything else is reported in `ack_error` rather than swallowed, because an ack that quietly
|
|
663
646
|
* did not land leaves the sender in the silence the whole feature exists to end.
|
|
664
647
|
*/
|
|
665
|
-
export async function start(ref, { cwd = process.cwd(),
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
await api.ack(id, true, "");
|
|
671
|
-
return { ...pulled, took: true, ack_error: "", own: false };
|
|
672
|
-
} catch (err) {
|
|
673
|
-
if (err.status === 403) return { ...pulled, took: false, ack_error: "", own: true };
|
|
674
|
-
return { ...pulled, took: false, ack_error: err.message, own: false };
|
|
675
|
-
}
|
|
648
|
+
export async function start(ref, { cwd = process.cwd(), save = false } = {}) {
|
|
649
|
+
const r = await take(await resolveId(ref), { cwd, save });
|
|
650
|
+
if (!r.guide?.markdown) return r;
|
|
651
|
+
return { ...r, markdown: r.guide.markdown };
|
|
676
652
|
}
|
|
677
653
|
|
|
678
654
|
/**
|
|
@@ -949,24 +925,11 @@ export async function parentGuide(
|
|
|
949
925
|
}
|
|
950
926
|
}
|
|
951
927
|
|
|
952
|
-
/**
|
|
953
|
-
export async function list(query = "", {
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
if (remote && api.loggedIn()) {
|
|
958
|
-
try {
|
|
959
|
-
const res = await api.list(query, scope);
|
|
960
|
-
for (const r of res.guides) {
|
|
961
|
-
const row = seen.has(r.id) ? rows.find((x) => x.id === r.id) : null;
|
|
962
|
-
if (row) Object.assign(row, r, { where: "synced" });
|
|
963
|
-
else rows.push({ ...r, where: "synced" });
|
|
964
|
-
}
|
|
965
|
-
} catch (err) {
|
|
966
|
-
rows.warning = `sync unavailable: ${err.message}`;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return rows.sort((a, b) => (b.created || "").localeCompare(a.created || ""));
|
|
928
|
+
/** Guides on the server this account can see, newest first, optionally filtered. */
|
|
929
|
+
export async function list(query = "", { scope = "" } = {}) {
|
|
930
|
+
needsSync("listing guides");
|
|
931
|
+
const res = await api.list(query, scope);
|
|
932
|
+
return res.guides.map((r) => ({ ...r, where: "synced" }));
|
|
970
933
|
}
|
|
971
934
|
|
|
972
935
|
/** Guides handed to you (or your teams) that you have not pulled yet. Needs sync. */
|
|
@@ -1007,64 +970,6 @@ export async function activity({ all = false, limit = 50 } = {}) {
|
|
|
1007
970
|
* `done` says "I implemented it", a verdict says "I tried it and it holds up" — or does not.
|
|
1008
971
|
* A failing verdict needs a note; "it doesn't work" without a reason helps nobody.
|
|
1009
972
|
*/
|
|
1010
|
-
/**
|
|
1011
|
-
* Say whether a guide held, and why at the length that takes.
|
|
1012
|
-
*
|
|
1013
|
-
* The reason used to be one field, capped at 280 characters by the row it was written for — so an
|
|
1014
|
-
* agent that had run the Verification and found it did not hold published a guide called
|
|
1015
|
-
* "Correction: …" instead, because measurements, commands and commit ids do not fit in a tweet.
|
|
1016
|
-
* The first line is the summary a row shows; the whole thing is what the guide shows.
|
|
1017
|
-
*/
|
|
1018
|
-
export async function verdict(id, ok, note = "", { images = [] } = {}) {
|
|
1019
|
-
if (!api.loggedIn()) throw new PassalongError("verdicts need sync — run `passalong login` first");
|
|
1020
|
-
const said = note.trim();
|
|
1021
|
-
if (!ok && !said)
|
|
1022
|
-
throw new PassalongError("say what went wrong: passalong broken <id> <what happened>");
|
|
1023
|
-
// "It works" is shown, not said: the server refuses one with no screenshot of it working, so
|
|
1024
|
-
// the refusal is said here before anything is uploaded, in the words of the command that fixes it.
|
|
1025
|
-
if (ok && !images.length && !/\/v1\/shots\/[a-z0-9]{6,16}/.test(said))
|
|
1026
|
-
throw new PassalongError(
|
|
1027
|
-
"show it working: passalong works <id> <screenshot.png> [what you checked]",
|
|
1028
|
-
);
|
|
1029
|
-
const guide = await resolveId(id);
|
|
1030
|
-
const shots = [];
|
|
1031
|
-
for (const file of images) shots.push((await attach(file)).markdown);
|
|
1032
|
-
const line = said.split("\n")[0].slice(0, 280);
|
|
1033
|
-
const detail = [said.length > line.length || shots.length ? said : "", ...shots]
|
|
1034
|
-
.filter(Boolean)
|
|
1035
|
-
.join("\n");
|
|
1036
|
-
return api.verdict(guide, ok, line, detail);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* The words after `passalong works <id>`, split into the screenshots and the note: an argument is a
|
|
1041
|
-
* screenshot when it names an image file that exists, and everything else is what was checked.
|
|
1042
|
-
*/
|
|
1043
|
-
export function proofArgs(args) {
|
|
1044
|
-
const images = [];
|
|
1045
|
-
const words = [];
|
|
1046
|
-
for (const a of args) {
|
|
1047
|
-
const ext = a.slice(a.lastIndexOf(".")).toLowerCase();
|
|
1048
|
-
if (SHOT_TYPES[ext] && existsSync(a)) images.push(a);
|
|
1049
|
-
else words.push(a);
|
|
1050
|
-
}
|
|
1051
|
-
return { images, note: words.join(" ") };
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/**
|
|
1055
|
-
* Say whether you are taking a guide someone handed you — before doing any of it.
|
|
1056
|
-
*
|
|
1057
|
-
* The hop the product had no signal for: until this, a handoff nobody had answered looked exactly
|
|
1058
|
-
* like one nobody had noticed. Passing needs a reason for the same cause a failing verdict does —
|
|
1059
|
-
* "not me" without "why" leaves the sender where the silence did.
|
|
1060
|
-
*/
|
|
1061
|
-
export async function ack(id, taken, note = "") {
|
|
1062
|
-
if (!api.loggedIn()) throw new PassalongError("acks need sync — run `passalong login` first");
|
|
1063
|
-
if (!taken && !note.trim())
|
|
1064
|
-
throw new PassalongError("say why you are passing: pass a note with taken=false");
|
|
1065
|
-
return api.ack(await resolveId(id), taken, note.trim());
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
973
|
/** Mark notifications seen. No ids means everything unread. */
|
|
1069
974
|
export const seen = (ids = []) => api.markRead(ids);
|
|
1070
975
|
|
|
@@ -1088,25 +993,20 @@ export function summary(g) {
|
|
|
1088
993
|
export async function setStatus(id, status) {
|
|
1089
994
|
if (!STATUSES.includes(status))
|
|
1090
995
|
throw new PassalongError(`status must be one of ${STATUSES.join(", ")}`);
|
|
1091
|
-
const { guide } = await resolve(id);
|
|
996
|
+
const { guide } = await resolve(id, { peek: true });
|
|
1092
997
|
const next = { ...guide, meta: { ...guide.meta, status } };
|
|
1093
998
|
// The server first. Saved the other way round, a refusal left the local copy saying what the
|
|
1094
999
|
// server had just refused — a teammate's `ready` on someone else's task read as done here.
|
|
1095
|
-
|
|
1096
|
-
store.save(next);
|
|
1000
|
+
await api.setStatus(id, status);
|
|
1097
1001
|
return next;
|
|
1098
1002
|
}
|
|
1099
1003
|
|
|
1100
1004
|
/**
|
|
1101
|
-
* Move a
|
|
1102
|
-
*
|
|
1103
|
-
* Only tasks. Every other kind is published the moment it is shared, so "ready" on one would
|
|
1104
|
-
* either do nothing or reopen something archived under a name that does not say so.
|
|
1005
|
+
* Move a guide from Draft to Ready: a person has read it, and whoever it is for may take it now.
|
|
1006
|
+
* Every kind: a handoff or bug is a draft until somebody says so, as a task always was.
|
|
1105
1007
|
*/
|
|
1106
1008
|
export async function ready(id) {
|
|
1107
|
-
const { guide } = await resolve(id);
|
|
1108
|
-
if (guide.meta.kind !== "task")
|
|
1109
|
-
throw new PassalongError(`${guide.meta.id || id} is not a task — ready is for tasks in Draft`);
|
|
1009
|
+
const { guide } = await resolve(id, { peek: true });
|
|
1110
1010
|
if (guide.meta.status !== "draft")
|
|
1111
1011
|
throw new PassalongError(`${guide.meta.id} is not in Draft (status: ${guide.meta.status})`);
|
|
1112
1012
|
return setStatus(guide.meta.id, "published");
|
|
@@ -1162,8 +1062,7 @@ export async function planTasks(steps, { cwd = process.cwd() } = {}) {
|
|
|
1162
1062
|
/** What an agent started by `work()` is told. The task itself goes in too, so it cannot start blind. */
|
|
1163
1063
|
export function workPrompt(t) {
|
|
1164
1064
|
return [
|
|
1165
|
-
`You hold passalong task ${t.id}, and no other agent can take it while you do
|
|
1166
|
-
`It is written to ${t.path}; here it is:`,
|
|
1065
|
+
`You hold passalong task ${t.id}, and no other agent can take it while you do. Here it is:`,
|
|
1167
1066
|
"",
|
|
1168
1067
|
t.markdown.trim(),
|
|
1169
1068
|
"",
|
|
@@ -1216,7 +1115,7 @@ export async function work({
|
|
|
1216
1115
|
const run = spawnSync(command, [...args, workPrompt(t)], {
|
|
1217
1116
|
cwd,
|
|
1218
1117
|
stdio: "inherit",
|
|
1219
|
-
env: { ...process.env, PASSALONG_TASK: t.id
|
|
1118
|
+
env: { ...process.env, PASSALONG_TASK: t.id },
|
|
1220
1119
|
});
|
|
1221
1120
|
const state = (await tasks()).find((x) => x.id === t.id)?.state;
|
|
1222
1121
|
if (state !== "review")
|
|
@@ -1233,14 +1132,14 @@ export async function work({
|
|
|
1233
1132
|
}
|
|
1234
1133
|
|
|
1235
1134
|
/**
|
|
1236
|
-
* Make every
|
|
1237
|
-
* planner has written several and you have read them all. Only your own:
|
|
1238
|
-
*
|
|
1135
|
+
* Make every guide you wrote that is still in Draft ready, and return their ids. For the moment a
|
|
1136
|
+
* planner has written several and you have read them all. Only your own: sending a guide on is its
|
|
1137
|
+
* author's call.
|
|
1239
1138
|
*/
|
|
1240
1139
|
export async function readyDrafts() {
|
|
1241
|
-
const drafts = (await
|
|
1242
|
-
for (const
|
|
1243
|
-
return drafts.map((
|
|
1140
|
+
const drafts = (await list("", { scope: "mine" })).filter((g) => g.status === "draft");
|
|
1141
|
+
for (const g of drafts) await ready(g.id);
|
|
1142
|
+
return drafts.map((g) => g.id);
|
|
1244
1143
|
}
|
|
1245
1144
|
|
|
1246
1145
|
export async function remove(id) {
|
|
@@ -1255,14 +1154,49 @@ export async function remove(id) {
|
|
|
1255
1154
|
store.remove(id);
|
|
1256
1155
|
}
|
|
1257
1156
|
|
|
1258
|
-
/**
|
|
1259
|
-
|
|
1157
|
+
/**
|
|
1158
|
+
* Write every guide to a directory as plain markdown: signed in, every one the server lets this
|
|
1159
|
+
* account read, and the local store's own besides. Returns the paths written.
|
|
1160
|
+
*/
|
|
1161
|
+
export async function exportAll(dir) {
|
|
1260
1162
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1261
|
-
const paths =
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1163
|
+
const paths = new Map();
|
|
1164
|
+
if (api.loggedIn()) {
|
|
1165
|
+
let after = "";
|
|
1166
|
+
do {
|
|
1167
|
+
const page = await api.exportPage(after);
|
|
1168
|
+
for (const g of page.guides) paths.set(g.id, g.markdown);
|
|
1169
|
+
after = page.next;
|
|
1170
|
+
} while (after);
|
|
1171
|
+
}
|
|
1172
|
+
for (const g of store.list()) if (!paths.has(g.meta.id)) paths.set(g.meta.id, serialize(g));
|
|
1173
|
+
return [...paths].map(([id, md]) => {
|
|
1174
|
+
const p = join(dir, `${id}.md`);
|
|
1175
|
+
writeFileSync(p, md);
|
|
1176
|
+
return p;
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Once per machine, signed in: drop the local copies the server says it holds. They were written
|
|
1182
|
+
* by every share, pull and take before `keep()`, and nothing ever removed one. What the server
|
|
1183
|
+
* does not return — shared while signed out, or from another account — is left where it is.
|
|
1184
|
+
*/
|
|
1185
|
+
export async function tidy() {
|
|
1186
|
+
if (!api.loggedIn() || store.readConfig().tidied) return 0;
|
|
1187
|
+
const local = new Set(store.list().map((g) => g.meta.id));
|
|
1188
|
+
let removed = 0;
|
|
1189
|
+
let after = "";
|
|
1190
|
+
while (local.size) {
|
|
1191
|
+
const page = await api.exportPage(after);
|
|
1192
|
+
for (const g of page.guides)
|
|
1193
|
+
if (local.delete(g.id)) {
|
|
1194
|
+
store.remove(g.id);
|
|
1195
|
+
removed++;
|
|
1196
|
+
}
|
|
1197
|
+
after = page.next;
|
|
1198
|
+
if (!after) break;
|
|
1266
1199
|
}
|
|
1267
|
-
|
|
1200
|
+
store.writeConfig({ tidied: true });
|
|
1201
|
+
return removed;
|
|
1268
1202
|
}
|