passalong 0.17.0 → 0.19.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 +71 -150
- package/package.json +1 -1
- package/skill/SKILL.md +6 -7
- package/src/api.js +18 -1
- package/src/guide.js +6 -4
- package/src/mcp.js +92 -49
- package/src/passalong.js +121 -182
package/src/passalong.js
CHANGED
|
@@ -78,26 +78,13 @@ 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
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let synced = false;
|
|
89
|
-
let notified = false;
|
|
90
|
-
if (api.loggedIn()) {
|
|
91
|
-
const res = await api.publish(guide.meta.id, serialize(guide));
|
|
92
|
-
url = res.url;
|
|
93
|
-
synced = true;
|
|
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
|
+
|
|
82
|
+
// The server is where a guide lives; nothing is kept on this machine.
|
|
83
|
+
needsSync("sharing a guide");
|
|
84
|
+
const res = await api.publish(guide.meta.id, serialize(guide));
|
|
85
|
+
const url = res.url;
|
|
86
|
+
guide = { ...guide, meta: { ...guide.meta, url } };
|
|
87
|
+
return { guide, url, synced: true, notified: Boolean(res.notified) };
|
|
101
88
|
}
|
|
102
89
|
|
|
103
90
|
/**
|
|
@@ -151,8 +138,11 @@ export async function fileBugs(
|
|
|
151
138
|
return { report: { id: report.id, title, environment, team }, issues: filed };
|
|
152
139
|
}
|
|
153
140
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a reference (id, share URL, or local file path) to a guide. An id is the server's to
|
|
143
|
+
* answer: nothing keeps a copy on this machine. `peek` reads without recording a pull.
|
|
144
|
+
*/
|
|
145
|
+
export async function resolve(ref, { peek = false } = {}) {
|
|
156
146
|
if (/^https?:\/\//.test(ref)) {
|
|
157
147
|
const md = await api.fetchShared(ref);
|
|
158
148
|
return { guide: parse(md), from: "link" };
|
|
@@ -162,21 +152,13 @@ export async function resolve(ref) {
|
|
|
162
152
|
}
|
|
163
153
|
if (!ID_RE.test(ref))
|
|
164
154
|
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
|
-
}
|
|
155
|
+
needsSync("reading a guide by id");
|
|
156
|
+
try {
|
|
157
|
+
return { guide: parse(await api.get(ref, { peek })), from: "sync" };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err.status === 404) throw new PassalongError(`no guide "${ref}"`);
|
|
160
|
+
throw err;
|
|
176
161
|
}
|
|
177
|
-
throw new PassalongError(
|
|
178
|
-
`no guide "${ref}" (not local${api.loggedIn() ? " or synced" : "; not logged in, so sync was not checked"})`,
|
|
179
|
-
);
|
|
180
162
|
}
|
|
181
163
|
|
|
182
164
|
/**
|
|
@@ -201,22 +183,21 @@ export async function resolveId(ref) {
|
|
|
201
183
|
}
|
|
202
184
|
|
|
203
185
|
/**
|
|
204
|
-
* Pull a guide
|
|
205
|
-
*
|
|
206
|
-
*
|
|
186
|
+
* Pull a guide and return it, so the caller can put the text straight into an agent's context. A
|
|
187
|
+
* pull of someone else's guide is recorded server-side, which is how the sender sees the transfer
|
|
188
|
+
* landed. Nothing is written unless `save` asks: then it lands in `.passalong/<id>.md` here.
|
|
207
189
|
*/
|
|
208
|
-
export async function pull(ref, { cwd = process.cwd(),
|
|
190
|
+
export async function pull(ref, { cwd = process.cwd(), save = false } = {}) {
|
|
209
191
|
const { guide, from } = await resolve(ref);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return { guide, from, path, markdown: serialize(guide) };
|
|
192
|
+
const markdown = serialize(guide);
|
|
193
|
+
return { guide, from, path: save ? saveTo(cwd, guide.meta.id, markdown) : null, markdown };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Write a guide into `.passalong/<id>.md` under `cwd`, when somebody asked for a file. */
|
|
197
|
+
function saveTo(cwd, id, markdown) {
|
|
198
|
+
const path = join(localDir(cwd), `${id}.md`);
|
|
199
|
+
writeFileSync(path, markdown);
|
|
200
|
+
return path;
|
|
220
201
|
}
|
|
221
202
|
|
|
222
203
|
/**
|
|
@@ -284,17 +265,13 @@ export async function tasks() {
|
|
|
284
265
|
|
|
285
266
|
/**
|
|
286
267
|
* 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.
|
|
268
|
+
* `null` when there is nothing to do. Written to `.passalong/<id>.md` only when `save` asks.
|
|
288
269
|
*/
|
|
289
|
-
export async function nextTask({ cwd = process.cwd(), any = false } = {}) {
|
|
270
|
+
export async function nextTask({ cwd = process.cwd(), any = false, save = false } = {}) {
|
|
290
271
|
needsSync("taking a task");
|
|
291
272
|
const { task } = await api.nextTask({ ...agent(cwd), any });
|
|
292
273
|
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 };
|
|
274
|
+
return { ...task, path: save ? saveTo(agent(cwd).worktree, task.id, task.markdown) : null };
|
|
298
275
|
}
|
|
299
276
|
|
|
300
277
|
/** Word from the agent holding a task: renews its lease, and `note` becomes the board's line. */
|
|
@@ -397,18 +374,15 @@ export async function finishTask(
|
|
|
397
374
|
* `next`, the calls that make sense from here, and `say` when the move is to stop.
|
|
398
375
|
*
|
|
399
376
|
* 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
|
|
377
|
+
* document comes back in the answer, and is written to `.passalong/<id>.md` only when `save` asks.
|
|
401
378
|
*/
|
|
402
|
-
export async function take(id, { cwd = process.cwd(), any = false } = {}) {
|
|
379
|
+
export async function take(id, { cwd = process.cwd(), any = false, save = false } = {}) {
|
|
403
380
|
needsSync("taking work");
|
|
404
381
|
const who = agent(cwd);
|
|
405
382
|
const r = await api.take({ ...who, ...(id ? { id: await resolveId(id) } : {}), any });
|
|
406
383
|
if (!r.guide?.markdown) return r;
|
|
407
384
|
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 };
|
|
385
|
+
return { ...r, path: save ? saveTo(who.worktree, r.guide.id, r.guide.markdown) : null };
|
|
412
386
|
}
|
|
413
387
|
|
|
414
388
|
/** Word from the agent that holds it: the lease starts again, and `note` is what the hub shows. */
|
|
@@ -603,6 +577,14 @@ export async function now({ cwd = process.cwd() } = {}) {
|
|
|
603
577
|
return { held, waiting, needs: review + handed + waiting.inbox, agent: who.agent };
|
|
604
578
|
}
|
|
605
579
|
|
|
580
|
+
/** Finished work back to To do, with why (docs/REVIEW.md §4). */
|
|
581
|
+
export async function reopen(id, why) {
|
|
582
|
+
needsSync("reopening work");
|
|
583
|
+
if (!String(why || "").trim())
|
|
584
|
+
throw new PassalongError("say why: whoever takes it next reads it");
|
|
585
|
+
return api.reopen(await resolveId(id), why);
|
|
586
|
+
}
|
|
587
|
+
|
|
606
588
|
/**
|
|
607
589
|
* The gate, a person's three answers to a task. Approve: the work in review is done. Reject: it
|
|
608
590
|
* goes back to Ready with `why` on it for the next agent. Release: a task an agent holds, live or
|
|
@@ -613,16 +595,19 @@ export async function now({ cwd = process.cwd() } = {}) {
|
|
|
613
595
|
* line the next reader needs.
|
|
614
596
|
*/
|
|
615
597
|
export async function approveTask(id) {
|
|
616
|
-
needsSync("approving
|
|
617
|
-
const
|
|
598
|
+
needsSync("approving work");
|
|
599
|
+
const { guide } = await resolve(id, { peek: true });
|
|
600
|
+
const r = await (guide.meta.kind === "task" ? api.approveTask(id) : api.closeGuide(id));
|
|
618
601
|
store.remove(id);
|
|
619
602
|
return r;
|
|
620
603
|
}
|
|
621
604
|
|
|
622
605
|
export async function rejectTask(id, why) {
|
|
623
|
-
needsSync("
|
|
624
|
-
if (!String(why || "").trim())
|
|
625
|
-
|
|
606
|
+
needsSync("sending work back");
|
|
607
|
+
if (!String(why || "").trim())
|
|
608
|
+
throw new PassalongError("say why: whoever takes it next reads it");
|
|
609
|
+
const { guide } = await resolve(id, { peek: true });
|
|
610
|
+
const r = await (guide.meta.kind === "task" ? api.rejectTask(id, why) : api.sendBack(id, why));
|
|
626
611
|
store.remove(id);
|
|
627
612
|
return r;
|
|
628
613
|
}
|
|
@@ -662,17 +647,10 @@ export async function release(id) {
|
|
|
662
647
|
* - anything else is reported in `ack_error` rather than swallowed, because an ack that quietly
|
|
663
648
|
* did not land leaves the sender in the silence the whole feature exists to end.
|
|
664
649
|
*/
|
|
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
|
-
}
|
|
650
|
+
export async function start(ref, { cwd = process.cwd(), save = false } = {}) {
|
|
651
|
+
const r = await take(await resolveId(ref), { cwd, save });
|
|
652
|
+
if (!r.guide?.markdown) return r;
|
|
653
|
+
return { ...r, markdown: r.guide.markdown };
|
|
676
654
|
}
|
|
677
655
|
|
|
678
656
|
/**
|
|
@@ -949,24 +927,11 @@ export async function parentGuide(
|
|
|
949
927
|
}
|
|
950
928
|
}
|
|
951
929
|
|
|
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 || ""));
|
|
930
|
+
/** Guides on the server this account can see, newest first, optionally filtered. */
|
|
931
|
+
export async function list(query = "", { scope = "" } = {}) {
|
|
932
|
+
needsSync("listing guides");
|
|
933
|
+
const res = await api.list(query, scope);
|
|
934
|
+
return res.guides.map((r) => ({ ...r, where: "synced" }));
|
|
970
935
|
}
|
|
971
936
|
|
|
972
937
|
/** Guides handed to you (or your teams) that you have not pulled yet. Needs sync. */
|
|
@@ -1007,64 +972,6 @@ export async function activity({ all = false, limit = 50 } = {}) {
|
|
|
1007
972
|
* `done` says "I implemented it", a verdict says "I tried it and it holds up" — or does not.
|
|
1008
973
|
* A failing verdict needs a note; "it doesn't work" without a reason helps nobody.
|
|
1009
974
|
*/
|
|
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
975
|
/** Mark notifications seen. No ids means everything unread. */
|
|
1069
976
|
export const seen = (ids = []) => api.markRead(ids);
|
|
1070
977
|
|
|
@@ -1088,25 +995,20 @@ export function summary(g) {
|
|
|
1088
995
|
export async function setStatus(id, status) {
|
|
1089
996
|
if (!STATUSES.includes(status))
|
|
1090
997
|
throw new PassalongError(`status must be one of ${STATUSES.join(", ")}`);
|
|
1091
|
-
const { guide } = await resolve(id);
|
|
998
|
+
const { guide } = await resolve(id, { peek: true });
|
|
1092
999
|
const next = { ...guide, meta: { ...guide.meta, status } };
|
|
1093
1000
|
// The server first. Saved the other way round, a refusal left the local copy saying what the
|
|
1094
1001
|
// server had just refused — a teammate's `ready` on someone else's task read as done here.
|
|
1095
|
-
|
|
1096
|
-
store.save(next);
|
|
1002
|
+
await api.setStatus(id, status);
|
|
1097
1003
|
return next;
|
|
1098
1004
|
}
|
|
1099
1005
|
|
|
1100
1006
|
/**
|
|
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.
|
|
1007
|
+
* Move a guide from Draft to Ready: a person has read it, and whoever it is for may take it now.
|
|
1008
|
+
* Every kind: a handoff or bug is a draft until somebody says so, as a task always was.
|
|
1105
1009
|
*/
|
|
1106
1010
|
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`);
|
|
1011
|
+
const { guide } = await resolve(id, { peek: true });
|
|
1110
1012
|
if (guide.meta.status !== "draft")
|
|
1111
1013
|
throw new PassalongError(`${guide.meta.id} is not in Draft (status: ${guide.meta.status})`);
|
|
1112
1014
|
return setStatus(guide.meta.id, "published");
|
|
@@ -1146,10 +1048,13 @@ export async function planTasks(steps, { cwd = process.cwd() } = {}) {
|
|
|
1146
1048
|
].join("\n");
|
|
1147
1049
|
// The summary is the step's own, said by whoever planned it: it is what a person reads about the
|
|
1148
1050
|
// task, and this builds the document without guessing one (the server refuses a task without).
|
|
1051
|
+
// A plan is the case Draft is for: several tasks at once, which a person reads before any of
|
|
1052
|
+
// them runs. Stated here, by the code that knows it is building one.
|
|
1149
1053
|
const meta = {
|
|
1150
1054
|
title: step.title,
|
|
1151
1055
|
summary: step.summary,
|
|
1152
1056
|
kind: "task",
|
|
1057
|
+
status: "draft",
|
|
1153
1058
|
target_context: step.target_context ?? repo,
|
|
1154
1059
|
};
|
|
1155
1060
|
const after = (step.after || []).map((j) => ids[j]);
|
|
@@ -1162,8 +1067,7 @@ export async function planTasks(steps, { cwd = process.cwd() } = {}) {
|
|
|
1162
1067
|
/** What an agent started by `work()` is told. The task itself goes in too, so it cannot start blind. */
|
|
1163
1068
|
export function workPrompt(t) {
|
|
1164
1069
|
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:`,
|
|
1070
|
+
`You hold passalong task ${t.id}, and no other agent can take it while you do. Here it is:`,
|
|
1167
1071
|
"",
|
|
1168
1072
|
t.markdown.trim(),
|
|
1169
1073
|
"",
|
|
@@ -1216,7 +1120,7 @@ export async function work({
|
|
|
1216
1120
|
const run = spawnSync(command, [...args, workPrompt(t)], {
|
|
1217
1121
|
cwd,
|
|
1218
1122
|
stdio: "inherit",
|
|
1219
|
-
env: { ...process.env, PASSALONG_TASK: t.id
|
|
1123
|
+
env: { ...process.env, PASSALONG_TASK: t.id },
|
|
1220
1124
|
});
|
|
1221
1125
|
const state = (await tasks()).find((x) => x.id === t.id)?.state;
|
|
1222
1126
|
if (state !== "review")
|
|
@@ -1233,14 +1137,14 @@ export async function work({
|
|
|
1233
1137
|
}
|
|
1234
1138
|
|
|
1235
1139
|
/**
|
|
1236
|
-
* Make every
|
|
1237
|
-
* planner has written several and you have read them all. Only your own:
|
|
1238
|
-
*
|
|
1140
|
+
* Make every guide you wrote that is still in Draft ready, and return their ids. For the moment a
|
|
1141
|
+
* planner has written several and you have read them all. Only your own: sending a guide on is its
|
|
1142
|
+
* author's call.
|
|
1239
1143
|
*/
|
|
1240
1144
|
export async function readyDrafts() {
|
|
1241
|
-
const drafts = (await
|
|
1242
|
-
for (const
|
|
1243
|
-
return drafts.map((
|
|
1145
|
+
const drafts = (await list("", { scope: "mine" })).filter((g) => g.status === "draft");
|
|
1146
|
+
for (const g of drafts) await ready(g.id);
|
|
1147
|
+
return drafts.map((g) => g.id);
|
|
1244
1148
|
}
|
|
1245
1149
|
|
|
1246
1150
|
export async function remove(id) {
|
|
@@ -1255,14 +1159,49 @@ export async function remove(id) {
|
|
|
1255
1159
|
store.remove(id);
|
|
1256
1160
|
}
|
|
1257
1161
|
|
|
1258
|
-
/**
|
|
1259
|
-
|
|
1162
|
+
/**
|
|
1163
|
+
* Write every guide to a directory as plain markdown: signed in, every one the server lets this
|
|
1164
|
+
* account read, and the local store's own besides. Returns the paths written.
|
|
1165
|
+
*/
|
|
1166
|
+
export async function exportAll(dir) {
|
|
1260
1167
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1261
|
-
const paths =
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1168
|
+
const paths = new Map();
|
|
1169
|
+
if (api.loggedIn()) {
|
|
1170
|
+
let after = "";
|
|
1171
|
+
do {
|
|
1172
|
+
const page = await api.exportPage(after);
|
|
1173
|
+
for (const g of page.guides) paths.set(g.id, g.markdown);
|
|
1174
|
+
after = page.next;
|
|
1175
|
+
} while (after);
|
|
1176
|
+
}
|
|
1177
|
+
for (const g of store.list()) if (!paths.has(g.meta.id)) paths.set(g.meta.id, serialize(g));
|
|
1178
|
+
return [...paths].map(([id, md]) => {
|
|
1179
|
+
const p = join(dir, `${id}.md`);
|
|
1180
|
+
writeFileSync(p, md);
|
|
1181
|
+
return p;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Once per machine, signed in: drop the local copies the server says it holds. They were written
|
|
1187
|
+
* by every share, pull and take before `keep()`, and nothing ever removed one. What the server
|
|
1188
|
+
* does not return — shared while signed out, or from another account — is left where it is.
|
|
1189
|
+
*/
|
|
1190
|
+
export async function tidy() {
|
|
1191
|
+
if (!api.loggedIn() || store.readConfig().tidied) return 0;
|
|
1192
|
+
const local = new Set(store.list().map((g) => g.meta.id));
|
|
1193
|
+
let removed = 0;
|
|
1194
|
+
let after = "";
|
|
1195
|
+
while (local.size) {
|
|
1196
|
+
const page = await api.exportPage(after);
|
|
1197
|
+
for (const g of page.guides)
|
|
1198
|
+
if (local.delete(g.id)) {
|
|
1199
|
+
store.remove(g.id);
|
|
1200
|
+
removed++;
|
|
1201
|
+
}
|
|
1202
|
+
after = page.next;
|
|
1203
|
+
if (!after) break;
|
|
1266
1204
|
}
|
|
1267
|
-
|
|
1205
|
+
store.writeConfig({ tidied: true });
|
|
1206
|
+
return removed;
|
|
1268
1207
|
}
|