passalong 0.6.0 → 0.9.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 +2 -2
- package/bin/passalong +227 -18
- package/package.json +1 -1
- package/skill/SKILL.md +28 -1
- package/src/api.js +82 -5
- package/src/checks.js +127 -0
- package/src/guide.js +27 -5
- package/src/hooks.js +8 -5
- package/src/mcp.js +278 -135
- package/src/passalong.js +183 -14
- package/src/update.js +116 -0
package/src/passalong.js
CHANGED
|
@@ -287,6 +287,61 @@ export async function taskProgress(id, note, { cwd = process.cwd() } = {}) {
|
|
|
287
287
|
return api.taskProgress(id, note ? { agent: a, note } : { agent: a });
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* What a hand-in has to carry, checked before anything is published.
|
|
292
|
+
*
|
|
293
|
+
* The server refuses the call without it anyway; the reason to check first is that publishing the
|
|
294
|
+
* write-up and then being refused leaves a guide nobody asked for behind every refused hand-in.
|
|
295
|
+
* The rule and the wording are the server's — see evidenceProblem() in apps/api/src/claims.ts.
|
|
296
|
+
*/
|
|
297
|
+
/**
|
|
298
|
+
* The claim generation this worktree was given when it took a guide, kept in `.passalong/held.json`
|
|
299
|
+
* beside `agent.json`. See apps/api/migrations/0029_claim_fence.sql.
|
|
300
|
+
*
|
|
301
|
+
* The agent never sees this number and is never asked for it — a model echoing an integer back
|
|
302
|
+
* three calls later is not a lock. `take` writes it, and every call that writes to the claim sends
|
|
303
|
+
* it, so a hand-in belonging to a claim that was released and re-taken is refused by the server
|
|
304
|
+
* rather than landing on somebody else's work.
|
|
305
|
+
*/
|
|
306
|
+
function fenceFile(cwd) {
|
|
307
|
+
return join(localDir(agent(cwd).worktree), "held.json");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function heldFences(cwd) {
|
|
311
|
+
try {
|
|
312
|
+
const held = JSON.parse(readFileSync(fenceFile(cwd), "utf8"));
|
|
313
|
+
return held && typeof held === "object" ? held : {};
|
|
314
|
+
} catch {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The number this worktree holds for a guide, or undefined when it has none to send. */
|
|
320
|
+
function fenceFor(id, cwd) {
|
|
321
|
+
const n = heldFences(cwd)[id];
|
|
322
|
+
return Number.isInteger(n) ? n : undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function rememberFence(id, fence, cwd) {
|
|
326
|
+
if (!Number.isInteger(fence)) return;
|
|
327
|
+
const held = heldFences(cwd);
|
|
328
|
+
held[id] = fence;
|
|
329
|
+
writeFileSync(fenceFile(cwd), `${JSON.stringify(held, null, 2)}\n`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** A check a runner executed: the command it ran, and what the process returned. See checks.js. */
|
|
333
|
+
const verified = (c) => Boolean(c?.cmd) && (typeof c?.exit === "number" || c?.exit === null);
|
|
334
|
+
|
|
335
|
+
function requireEvidence(evidence) {
|
|
336
|
+
const said = String(evidence || "").trim();
|
|
337
|
+
if (said.length >= 16) return;
|
|
338
|
+
throw new PassalongError(
|
|
339
|
+
"send `evidence`: what you ran and what came back — the command and the lines that decided " +
|
|
340
|
+
"it, a test summary, a link to the change, or a screenshot url. " +
|
|
341
|
+
`"${said || "nothing"}" is a claim, not evidence.`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
290
345
|
/**
|
|
291
346
|
* The work is done: the task moves to review with a transfer guide about it attached.
|
|
292
347
|
*
|
|
@@ -296,9 +351,10 @@ export async function taskProgress(id, note, { cwd = process.cwd() } = {}) {
|
|
|
296
351
|
*/
|
|
297
352
|
export async function finishTask(
|
|
298
353
|
id,
|
|
299
|
-
{ report, markdown, pr = "", note = "", cwd = process.cwd() } = {},
|
|
354
|
+
{ report, markdown, evidence = "", pr = "", note = "", cwd = process.cwd() } = {},
|
|
300
355
|
) {
|
|
301
356
|
needsSync("finishing a task");
|
|
357
|
+
requireEvidence(evidence);
|
|
302
358
|
if (!report && markdown) {
|
|
303
359
|
const g = parse(markdown);
|
|
304
360
|
report = (await share(serialize({ meta: { ...g.meta, parent: id }, body: g.body }), { cwd }))
|
|
@@ -308,7 +364,7 @@ export async function finishTask(
|
|
|
308
364
|
throw new PassalongError(
|
|
309
365
|
"finishing needs the write-up: `markdown` for a transfer guide about the work, or `report` with the id of one",
|
|
310
366
|
);
|
|
311
|
-
return api.finishTask(id, { agent: agent(cwd).agent, report, pr, note });
|
|
367
|
+
return api.finishTask(id, { agent: agent(cwd).agent, report, evidence, pr, note });
|
|
312
368
|
}
|
|
313
369
|
|
|
314
370
|
/**
|
|
@@ -323,6 +379,7 @@ export async function take(id, { cwd = process.cwd(), any = false } = {}) {
|
|
|
323
379
|
const who = agent(cwd);
|
|
324
380
|
const r = await api.take({ ...who, ...(id ? { id: await resolveId(id) } : {}), any });
|
|
325
381
|
if (!r.guide?.markdown) return r;
|
|
382
|
+
rememberFence(r.guide.id, r.guide.fence, cwd);
|
|
326
383
|
const path = join(localDir(who.worktree), `${r.guide.id}.md`);
|
|
327
384
|
writeFileSync(path, r.guide.markdown);
|
|
328
385
|
store.save(parse(r.guide.markdown));
|
|
@@ -332,38 +389,97 @@ export async function take(id, { cwd = process.cwd(), any = false } = {}) {
|
|
|
332
389
|
/** Word from the agent that holds it: the lease starts again, and `note` is what the hub shows. */
|
|
333
390
|
export async function progress(id, note, { cwd = process.cwd() } = {}) {
|
|
334
391
|
needsSync("reporting progress");
|
|
335
|
-
|
|
392
|
+
const fence = fenceFor(id, cwd);
|
|
393
|
+
return api.progress(id, {
|
|
394
|
+
agent: agent(cwd).agent,
|
|
395
|
+
...(note ? { note } : {}),
|
|
396
|
+
...(fence === undefined ? {} : { fence }),
|
|
397
|
+
});
|
|
336
398
|
}
|
|
337
399
|
|
|
338
400
|
/**
|
|
339
401
|
* Done here. A task hands in a write-up — `markdown`, published with `parent:` naming the task, or
|
|
340
402
|
* `report`, the id of one already published — for its author to review against Acceptance. Any
|
|
341
403
|
* other guide hands in whether its Verification held: `ok`, and a `note` when it did not.
|
|
404
|
+
*
|
|
405
|
+
* Every hand-in carries `evidence`: what was run and what came back. The write-up and the `ok` are
|
|
406
|
+
* both the agent's word for its own work; evidence is the part the reviewer can check.
|
|
342
407
|
*/
|
|
343
408
|
export async function handIn(
|
|
344
409
|
id,
|
|
345
|
-
{
|
|
410
|
+
{
|
|
411
|
+
ok,
|
|
412
|
+
note = "",
|
|
413
|
+
evidence = "",
|
|
414
|
+
checks = [],
|
|
415
|
+
writeup = "",
|
|
416
|
+
markdown,
|
|
417
|
+
report,
|
|
418
|
+
pr = "",
|
|
419
|
+
cwd = process.cwd(),
|
|
420
|
+
} = {},
|
|
346
421
|
) {
|
|
347
422
|
needsSync("handing work in");
|
|
423
|
+
// Checks are evidence, sorted against the lines they answer: bringing them is bringing it. A
|
|
424
|
+
// check whose command was actually run is exempt from the length rule — its exit code is the
|
|
425
|
+
// evidence, and `test -f dist/app.js` exiting 0 says more than any sentence about it would.
|
|
426
|
+
if (checks.length) checks.forEach((c) => (verified(c) ? null : requireEvidence(c?.ran)));
|
|
427
|
+
else requireEvidence(evidence);
|
|
348
428
|
if (!report && markdown) {
|
|
349
429
|
const g = parse(markdown);
|
|
350
430
|
report = (await share(serialize({ meta: { ...g.meta, parent: id }, body: g.body }), { cwd }))
|
|
351
431
|
.guide.meta.id;
|
|
352
432
|
}
|
|
433
|
+
const fence = fenceFor(id, cwd);
|
|
353
434
|
return api.handIn(id, {
|
|
354
435
|
agent: agent(cwd).agent,
|
|
355
436
|
note,
|
|
437
|
+
evidence,
|
|
438
|
+
...(fence === undefined ? {} : { fence }),
|
|
439
|
+
...(checks.length
|
|
440
|
+
? {
|
|
441
|
+
checks: checks.map((c) => ({
|
|
442
|
+
check: c.check,
|
|
443
|
+
ran: c.ran,
|
|
444
|
+
// Only present on a check that was run. The server keeps them apart the same way.
|
|
445
|
+
...(verified(c) ? { cmd: c.cmd, exit: c.exit, ok: c.ok } : {}),
|
|
446
|
+
})),
|
|
447
|
+
}
|
|
448
|
+
: {}),
|
|
356
449
|
...(report ? { report, pr } : {}),
|
|
450
|
+
// Left out when there is nothing to say, rather than sent as "": most hand-ins have nothing
|
|
451
|
+
// to adapt, and the field is for the ones that do.
|
|
452
|
+
...(writeup ? { writeup } : {}),
|
|
357
453
|
...(typeof ok === "boolean" ? { ok } : {}),
|
|
358
454
|
});
|
|
359
455
|
}
|
|
360
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Give a guide or task you wrote to someone else in its team: `@handle` for one person, `#group`
|
|
459
|
+
* for the people who do a thing, or "team" (or empty) for everyone. Whoever held it and is left out
|
|
460
|
+
* has it taken back and is told; a task then only goes to the new assignee's agents. The local copy
|
|
461
|
+
* is dropped so the next read shows the new `to:`.
|
|
462
|
+
*/
|
|
463
|
+
export async function assign(ref, to) {
|
|
464
|
+
needsSync("reassigning");
|
|
465
|
+
const id = await resolveId(ref);
|
|
466
|
+
const target = !to || to === "team" || to === "everyone" ? "" : String(to).trim();
|
|
467
|
+
const r = await api.assign(id, target);
|
|
468
|
+
store.remove(id);
|
|
469
|
+
return r;
|
|
470
|
+
}
|
|
471
|
+
|
|
361
472
|
/** Not this agent's to do: what it held is open again, and `why` goes to whoever is next. */
|
|
362
473
|
export async function pass(id, why, { cwd = process.cwd() } = {}) {
|
|
363
474
|
needsSync("passing work");
|
|
364
475
|
if (!String(why || "").trim())
|
|
365
476
|
throw new PassalongError("say why you are passing it, so whoever is next knows");
|
|
366
|
-
|
|
477
|
+
const fence = fenceFor(id, cwd);
|
|
478
|
+
return api.pass(id, {
|
|
479
|
+
agent: agent(cwd).agent,
|
|
480
|
+
why,
|
|
481
|
+
...(fence === undefined ? {} : { fence }),
|
|
482
|
+
});
|
|
367
483
|
}
|
|
368
484
|
|
|
369
485
|
/**
|
|
@@ -427,9 +543,16 @@ export async function rejectTask(id, why) {
|
|
|
427
543
|
return r;
|
|
428
544
|
}
|
|
429
545
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
546
|
+
/**
|
|
547
|
+
* Take it back from whoever is holding it — a task, a handoff or a bug, and from all of them at
|
|
548
|
+
* once when several people hold the same handoff in different repos.
|
|
549
|
+
*
|
|
550
|
+
* It was tasks only, which left an asymmetry nobody chose: the author of a handoff could mark it
|
|
551
|
+
* done or give it to somebody else, and had no way to simply have it back.
|
|
552
|
+
*/
|
|
553
|
+
export async function release(id) {
|
|
554
|
+
needsSync("taking work back");
|
|
555
|
+
const r = await api.release(id);
|
|
433
556
|
store.remove(id);
|
|
434
557
|
return r;
|
|
435
558
|
}
|
|
@@ -605,6 +728,39 @@ export async function followUps(meta = {}, deps = {}) {
|
|
|
605
728
|
].join("\n\n");
|
|
606
729
|
}
|
|
607
730
|
|
|
731
|
+
/**
|
|
732
|
+
* The guide a follow-up came out of, formatted to go in front of it — or "" when there is none.
|
|
733
|
+
*
|
|
734
|
+
* Follow-ups travelled one way: opening a guide handed over what was written under it, and opening
|
|
735
|
+
* one of those handed over nothing. An agent given a follow-up on its own reads a document that
|
|
736
|
+
* assumes a piece of work it has never seen, and either goes looking for it or treats the note as
|
|
737
|
+
* the whole job. Where the parent has got to is part of the context, not a separate question.
|
|
738
|
+
*
|
|
739
|
+
* The same shape the hosted server's parentOf() returns. Injectable for testing, and never worth
|
|
740
|
+
* failing the call over: the guide itself is what was asked for.
|
|
741
|
+
*/
|
|
742
|
+
export async function parentGuide(
|
|
743
|
+
meta = {},
|
|
744
|
+
{ parent = api.parent, loggedIn = api.loggedIn } = {},
|
|
745
|
+
) {
|
|
746
|
+
if (!meta.id || !meta.parent || !loggedIn()) return "";
|
|
747
|
+
try {
|
|
748
|
+
const { guide } = await parent(meta.id, { markdown: true });
|
|
749
|
+
if (!guide?.id) return "";
|
|
750
|
+
const who = guide.by?.name || (guide.by?.handle ? `@${guide.by.handle}` : "");
|
|
751
|
+
const state = guide.state === "held" && who ? `held by ${who}` : guide.state || "open";
|
|
752
|
+
return (
|
|
753
|
+
`THIS IS A FOLLOW-UP TO ${guide.id}: ${guide.title || "untitled"} — ${state}. It is more ` +
|
|
754
|
+
"context for that guide, not a piece of work on its own. Read the guide it follows first; " +
|
|
755
|
+
"it is below, and where the two disagree this follow-up is newer. If what it asks for " +
|
|
756
|
+
`depends on ${guide.id} being done and it is not, say so rather than starting.\n\n` +
|
|
757
|
+
`--- the guide it follows: ${guide.id} ---\n${String(guide.markdown ?? "").trimEnd()}\n\n`
|
|
758
|
+
);
|
|
759
|
+
} catch {
|
|
760
|
+
return "";
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
608
764
|
/** Local guides merged with synced ones (by id), newest first, optionally filtered. */
|
|
609
765
|
export async function list(query = "", { remote = true, scope = "" } = {}) {
|
|
610
766
|
const local = scope && scope !== "all" && scope !== "mine" ? [] : store.search(query);
|
|
@@ -663,11 +819,21 @@ export async function activity({ all = false, limit = 50 } = {}) {
|
|
|
663
819
|
* `done` says "I implemented it", a verdict says "I tried it and it holds up" — or does not.
|
|
664
820
|
* A failing verdict needs a note; "it doesn't work" without a reason helps nobody.
|
|
665
821
|
*/
|
|
822
|
+
/**
|
|
823
|
+
* Say whether a guide held, and why at the length that takes.
|
|
824
|
+
*
|
|
825
|
+
* The reason used to be one field, capped at 280 characters by the row it was written for — so an
|
|
826
|
+
* agent that had run the Verification and found it did not hold published a guide called
|
|
827
|
+
* "Correction: …" instead, because measurements, commands and commit ids do not fit in a tweet.
|
|
828
|
+
* The first line is the summary a row shows; the whole thing is what the guide shows.
|
|
829
|
+
*/
|
|
666
830
|
export async function verdict(id, ok, note = "") {
|
|
667
831
|
if (!api.loggedIn()) throw new PassalongError("verdicts need sync — run `passalong login` first");
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
832
|
+
const said = note.trim();
|
|
833
|
+
if (!ok && !said)
|
|
834
|
+
throw new PassalongError("say what went wrong: passalong broken <id> <what happened>");
|
|
835
|
+
const line = said.split("\n")[0].slice(0, 280);
|
|
836
|
+
return api.verdict(await resolveId(id), ok, line, said.length > line.length ? said : "");
|
|
671
837
|
}
|
|
672
838
|
|
|
673
839
|
/**
|
|
@@ -785,9 +951,12 @@ export function workPrompt(t) {
|
|
|
785
951
|
"When every Acceptance check holds, commit what you changed as one commit whose message",
|
|
786
952
|
`starts with "${t.id}: " — several tasks share this worktree, and a reviewer reads each one's`,
|
|
787
953
|
"change on its own. Then call hand_in with id",
|
|
788
|
-
`${t.id}, \`pr\` set to that commit's hash,
|
|
789
|
-
"did, what you decided and why, and how you checked each Acceptance line
|
|
790
|
-
"
|
|
954
|
+
`${t.id}, \`pr\` set to that commit's hash, \`markdown\`: a transfer guide saying what you`,
|
|
955
|
+
"did, what you decided and why, and how you checked each Acceptance line, and `evidence`:",
|
|
956
|
+
"what you ran and what came back — the commands and the lines that decided it, the test",
|
|
957
|
+
"summary, the commit. Keep that as you go; at the end you would be writing it from memory,",
|
|
958
|
+
"and it is the one part of the hand-in a reviewer can check. If either call says you no",
|
|
959
|
+
"longer hold the task, stop.",
|
|
791
960
|
"",
|
|
792
961
|
"Nobody reads what you print: this session runs unattended. If something only a person can",
|
|
793
962
|
"do stands between you and an Acceptance line — a permission, a secret, a decision — call",
|
package/src/update.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Telling somebody their CLI is old, without ever making them wait to be told.
|
|
2
|
+
//
|
|
3
|
+
// The server can ship a new version of itself in an afternoon; an installed CLI changes only when
|
|
4
|
+
// a person types `npm i -g passalong`. Nothing told them to, so a machine could sit on a build from
|
|
5
|
+
// months ago and simply not have the tools everyone was talking about — and because the API keeps
|
|
6
|
+
// answering old clients on purpose (an absent `fence` is accepted, an absent `cmd` is accepted),
|
|
7
|
+
// nothing broke loudly enough to notice either.
|
|
8
|
+
//
|
|
9
|
+
// The registry is asked, not passalong.dev. What "the latest passalong" is, is a fact about npm,
|
|
10
|
+
// and a server that answered it would be a second copy of that fact to keep in step.
|
|
11
|
+
//
|
|
12
|
+
// **Nothing here waits on the network.** The first attempt at this awaited the fetch after the
|
|
13
|
+
// command finished, on a 1.5s timeout, and measuring it killed the design: the registry answered in
|
|
14
|
+
// six to ten seconds from an ordinary connection, so every check timed out, cached nothing, and the
|
|
15
|
+
// feature would have shipped dead while looking fine in review. So the line is read from a file and
|
|
16
|
+
// the file is refreshed by a process nobody waits for. What a person sees is a day old at worst,
|
|
17
|
+
// which is the right age for "there is a new version".
|
|
18
|
+
import { readConfig, writeConfig } from "./store.js";
|
|
19
|
+
|
|
20
|
+
/** How long a cached answer lasts before a refresh is worth starting. */
|
|
21
|
+
export const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How long after a failed check before trying again. A refresh that could not reach the registry
|
|
25
|
+
* must not blind the check for a whole day — that is one bad minute costing a day of notices — and
|
|
26
|
+
* must not retry on every command either, which is what the whole-day gap is there to prevent.
|
|
27
|
+
*/
|
|
28
|
+
export const RETRY_AFTER_MS = 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
/** Nobody is waiting, so this can be patient. It is generous because the registry is slow. */
|
|
31
|
+
const TIMEOUT_MS = 15_000;
|
|
32
|
+
|
|
33
|
+
const REGISTRY = "https://registry.npmjs.org/passalong/latest";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is `latest` past `current`? Compares the numeric parts and nothing else.
|
|
37
|
+
*
|
|
38
|
+
* A prerelease (`1.2.0-rc.1`) is read as its release: it only matters that the numbers moved, and
|
|
39
|
+
* nobody is being told to install a tag that `latest` does not point at anyway.
|
|
40
|
+
*/
|
|
41
|
+
export function isNewer(latest, current) {
|
|
42
|
+
const parts = (v) =>
|
|
43
|
+
String(v ?? "")
|
|
44
|
+
.split("-")[0]
|
|
45
|
+
.split(".")
|
|
46
|
+
.map((n) => Number.parseInt(n, 10));
|
|
47
|
+
const a = parts(latest);
|
|
48
|
+
const b = parts(current);
|
|
49
|
+
if (a.length !== 3 || b.length !== 3 || [...a, ...b].some(Number.isNaN)) return false;
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
if (a[i] > b[i]) return true;
|
|
52
|
+
if (a[i] < b[i]) return false;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The one line somebody sees, or "" when there is nothing to say. */
|
|
58
|
+
export function noticeFor(latest, current) {
|
|
59
|
+
if (!isNewer(latest, current)) return "";
|
|
60
|
+
return `passalong ${latest} is out (you have ${current}) — npm i -g passalong`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What to print and whether a refresh is due, from the file alone. No network, no waiting.
|
|
65
|
+
*
|
|
66
|
+
* `PASSALONG_NO_UPDATE_CHECK=1` turns the whole thing off, for a machine that should not be
|
|
67
|
+
* talking to the registry at all — an air-gapped box, or a CI runner where the line is noise.
|
|
68
|
+
*/
|
|
69
|
+
export function cachedNotice({
|
|
70
|
+
current,
|
|
71
|
+
now = Date.now(),
|
|
72
|
+
env = process.env,
|
|
73
|
+
config = readConfig,
|
|
74
|
+
}) {
|
|
75
|
+
if (env.PASSALONG_NO_UPDATE_CHECK === "1") return { notice: "", stale: false };
|
|
76
|
+
const held = config();
|
|
77
|
+
const checked = Number(held.update_checked) || 0;
|
|
78
|
+
const failed = !held.update_latest;
|
|
79
|
+
const wait = failed ? RETRY_AFTER_MS : CHECK_EVERY_MS;
|
|
80
|
+
return {
|
|
81
|
+
notice: noticeFor(held.update_latest || "", current),
|
|
82
|
+
stale: now - checked >= wait,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ask the registry and write the answer down. Run by a process nobody is waiting for.
|
|
88
|
+
*
|
|
89
|
+
* A failure still records the attempt, so the retry gap applies to it rather than every command
|
|
90
|
+
* trying again — `cachedNotice` reads an empty `update_latest` as a failure and uses the shorter
|
|
91
|
+
* gap.
|
|
92
|
+
*/
|
|
93
|
+
export async function refresh({ now = Date.now(), fetchImpl = fetch, save = writeConfig } = {}) {
|
|
94
|
+
let latest = "";
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetchImpl(REGISTRY, {
|
|
97
|
+
// Plain JSON. The abbreviated `application/vnd.npm.install-v1+json` type is for the whole
|
|
98
|
+
// packument at `/passalong`; on `/passalong/latest` the registry answers 406, which is how
|
|
99
|
+
// the first version of this managed to fail every single time.
|
|
100
|
+
headers: { accept: "application/json" },
|
|
101
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
102
|
+
});
|
|
103
|
+
if (res.ok) {
|
|
104
|
+
const body = await res.json();
|
|
105
|
+
if (typeof body?.version === "string") latest = body.version;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Offline, blocked, rate-limited, nonsense JSON: the attempt is recorded and nothing is said.
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
save({ update_checked: now, update_latest: latest });
|
|
112
|
+
} catch {
|
|
113
|
+
// A config that cannot be written means the next command tries again, which is harmless.
|
|
114
|
+
}
|
|
115
|
+
return latest;
|
|
116
|
+
}
|