@finbheara/archive-client 0.20.1 → 0.21.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 CHANGED
@@ -709,6 +709,8 @@ is passed to the dancer resolver as asked (`rel.dancers`, above).
709
709
  idq config repo <owner/name>
710
710
  idq list [--remote]
711
711
  idq fetch [--release <spec>] [--assets results,majors,<file>...]
712
+ idq update [--assets results,majors,<file>...]
713
+ idq use <spec> [--assets results,majors,<file>...]
712
714
  idq pin <spec> | unpin | current
713
715
  idq add <dir> --tag <tag>
714
716
  idq index [--release <spec>] [<name>...] [--force]
@@ -748,6 +750,24 @@ is passed to the dancer resolver as asked (`rel.dancers`, above).
748
750
  `stream` prints one JSON line per row (or entry), as it reads them. `dancer` prints the results of the one dancer where the answer is unique (every candidate's with
749
751
  `--rows`). `--json` prints the call's answer as it is. `--cache` and `--repo` override the defaults.
750
752
 
753
+ `update` moves to the newest published release, as a package manager's `update` moves to the newest
754
+ version: it fetches it (verified as every download is), builds every index it knows, and only then
755
+ pins it, so a fetch or index that fails leaves the old pin in force. `use <spec>` does the same for
756
+ the release a spec names, older or newer (`releases().use(spec)` in the API; `update` is
757
+ `use("latest")`).
758
+
759
+ A query on the default spec (`pinned`, given or not) whose release is older than the newest
760
+ published one prints ONE line on stderr, and changes neither the answer nor the exit code:
761
+
762
+ answered from <tag>. <newer tag> is available: run `idq update` to get the latest
763
+
764
+ The remote is asked at most once per hour: what it said and when are kept in `<cache>/LATEST.json`,
765
+ and a call inside the hour reads that file. An offline, failed or slow check says nothing, and a
766
+ call waits for one at most 1.5 s, while its own query runs. A release named with `--release` (a
767
+ tag, a cut, `latest`) is never warned about: it was asked for. `main(argv, { freshness })` takes
768
+ `false` to turn the check off, or a `floor` (a cut the caller requires, and why: where only the
769
+ floor is newer, the line says the cut is required, with the why, and to run `idq use <cut>`), the `command` the warning names and the clock.
770
+
751
771
  `resolve` speaks JSON lines: one answer per line out, and with no positional question the
752
772
  questions are JSON lines on stdin, one per line (`{feis, code, year?, title?}`,
753
773
  `{name, school?, age?, population?, oireachtas?}`,
@@ -3800,6 +3800,15 @@ class ReleaseManager2 {
3800
3800
  writePin(this.root, pin);
3801
3801
  return pin;
3802
3802
  }
3803
+ async use(spec, o = {}) {
3804
+ const was = readPin2(this.root)?.tag ?? null;
3805
+ const rel = await this.fetch(spec, o);
3806
+ const tag = rel.tag;
3807
+ rel.close();
3808
+ const { built } = await this.index(tag);
3809
+ const pin = await this.pin(tag);
3810
+ return { tag, was, pin, built };
3811
+ }
3803
3812
  unpin() {
3804
3813
  return clearPin(this.root);
3805
3814
  }
@@ -3937,4 +3946,93 @@ function releases2(opts = {}) {
3937
3946
  return new ReleaseManager2(opts);
3938
3947
  }
3939
3948
 
3940
- export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2 };
3949
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3950
+ import { join as join5 } from "path";
3951
+ var LATEST_CHECK_MS2 = 60 * 60 * 1000;
3952
+ var LATEST_CHECK_BUDGET_MS2 = 1500;
3953
+ var latestCheckPath2 = (root) => join5(root, "LATEST.json");
3954
+ function readLatestCheck2(root) {
3955
+ const p = latestCheckPath2(root);
3956
+ if (!existsSync6(p))
3957
+ return null;
3958
+ try {
3959
+ const v = JSON.parse(readFileSync5(p, "utf8"));
3960
+ if (typeof v.repo !== "string" || typeof v.checkedAt !== "string")
3961
+ return null;
3962
+ return {
3963
+ repo: v.repo,
3964
+ checkedAt: v.checkedAt,
3965
+ latest: typeof v.latest === "string" ? v.latest : null,
3966
+ tags: Array.isArray(v.tags) ? v.tags.filter((t) => typeof t === "string") : []
3967
+ };
3968
+ } catch {
3969
+ return null;
3970
+ }
3971
+ }
3972
+ var KEPT_TAGS = 50;
3973
+ async function latestCheck2(root, repo, remote, now = Date.now, intervalMs = LATEST_CHECK_MS2) {
3974
+ const kept = readLatestCheck2(root);
3975
+ const prior = kept !== null && kept.repo === repo ? kept : null;
3976
+ if (prior !== null) {
3977
+ const age = now() - Date.parse(prior.checkedAt);
3978
+ if (age >= 0 && age < intervalMs)
3979
+ return prior;
3980
+ }
3981
+ const checkedAt = new Date(now()).toISOString();
3982
+ const held = {
3983
+ repo,
3984
+ checkedAt,
3985
+ latest: prior?.latest ?? null,
3986
+ tags: prior?.tags ?? []
3987
+ };
3988
+ keep(root, held);
3989
+ let check;
3990
+ try {
3991
+ const tags = (await remote.tags(repo)).filter((t) => parseArchiveTag2(t) !== null).sort(compareTagsNewestFirst2);
3992
+ check = {
3993
+ repo,
3994
+ checkedAt,
3995
+ latest: pickTag2(tags, { kind: "latest" }),
3996
+ tags: tags.slice(0, KEPT_TAGS)
3997
+ };
3998
+ } catch {
3999
+ return held;
4000
+ }
4001
+ keep(root, check);
4002
+ return check;
4003
+ }
4004
+ function keep(root, check) {
4005
+ try {
4006
+ writeJsonAtomic2(latestCheckPath2(root), check);
4007
+ } catch {}
4008
+ }
4009
+ async function within(p, ms) {
4010
+ let timer;
4011
+ const late = new Promise((resolve) => {
4012
+ timer = setTimeout(() => resolve(null), Math.max(0, ms));
4013
+ timer.unref?.();
4014
+ });
4015
+ try {
4016
+ return await Promise.race([p, late]);
4017
+ } finally {
4018
+ clearTimeout(timer);
4019
+ }
4020
+ }
4021
+ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
4022
+ const a = parseArchiveTag2(answered);
4023
+ if (a === null)
4024
+ return null;
4025
+ const belowFloor = floor !== null && (a.cut === undefined || a.cut < floor.cut);
4026
+ const why = belowFloor ? ` (${floor.why})` : "";
4027
+ if (latest !== null && compareTagsNewestFirst2(answered, latest) > 0) {
4028
+ const lp = parseArchiveTag2(latest);
4029
+ if (!belowFloor || lp?.cut !== undefined && lp.cut >= (floor?.cut ?? 0))
4030
+ return `answered from ${answered}. ${latest} is available${why}: run \`${command} update\` to get the latest`;
4031
+ }
4032
+ if (!belowFloor)
4033
+ return null;
4034
+ const floorTag = pickTag2(tags, { kind: "cut", cut: floor.cut, fraction: 0 });
4035
+ return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
4036
+ }
4037
+
4038
+ export { DATABASES2, planAsset2, sha256File2, unzstdTo2, NAME_LEVELS2, nameLevel2, NO_CALENDAR2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, resolveTag2, Release2, openRelease2, openCached2, ReleaseManager2, releases2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };
package/dist/cli.js CHANGED
@@ -1,11 +1,19 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ import {
4
+ cacheRoot2,
5
+ repoOf2
6
+ } from "./chunks/index-hne1wadw.js";
3
7
  import {
4
8
  nameOrderOf,
9
+ defaultRemote,
5
10
  openRelease2,
6
- releases2
7
- } from "./chunks/index-6028k52f.js";
8
- import"./chunks/index-hne1wadw.js";
11
+ releases2,
12
+ LATEST_CHECK_BUDGET_MS2,
13
+ latestCheck2,
14
+ within,
15
+ staleWarning2
16
+ } from "./chunks/index-pfsxe80h.js";
9
17
  import"./chunks/index-hghdy980.js";
10
18
 
11
19
  var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
@@ -15,6 +23,9 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
15
23
  with --remote the published tags
16
24
  fetch [--release <spec>] [--assets results,majors,<file>...]
17
25
  download, verify and cache a release (default: results)
26
+ update [--assets ...] move to the newest published release: fetch it (verified),
27
+ build its indexes, then pin it
28
+ use <spec> [--assets ...] the same for the release a spec names
18
29
  pin <spec> the user-local pin: what a bare open (spec "pinned") opens
19
30
  unpin remove the pin (a bare open is then "latest")
20
31
  current the pin, and whether it is cached
@@ -99,6 +110,10 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
99
110
  population?, oireachtas?}, a registration: {name, school?,
100
111
  codes[], feis, year?, population?, oireachtas?})
101
112
 
113
+ A query on the default spec prints one warning line on stderr when its release is older than
114
+ the newest published one (the remote asked at most once per hour, <cache>/LATEST.json); the
115
+ answer and the exit code are unchanged, and an offline check says nothing.
116
+
102
117
  <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)`;
103
118
  var VALUE = new Set([
104
119
  "--cache",
@@ -405,6 +420,27 @@ function eventYearOf(p) {
405
420
  ...year !== undefined ? { year: Number(year) } : {}
406
421
  };
407
422
  }
423
+ var ANSWERING = new Set([
424
+ "current",
425
+ "capabilities",
426
+ "feis",
427
+ "series",
428
+ "school",
429
+ "dancer",
430
+ "results",
431
+ "majors",
432
+ "stream",
433
+ "records",
434
+ "feis-dates",
435
+ "tabulated",
436
+ "teams",
437
+ "events",
438
+ "teacher",
439
+ "adjudicator",
440
+ "comp",
441
+ "syllabus",
442
+ "resolve"
443
+ ]);
408
444
  async function main(argv, io = {}) {
409
445
  const say = io.say ?? ((s) => console.log(s));
410
446
  const log = io.log ?? ((s) => console.error(s));
@@ -426,6 +462,23 @@ async function main(argv, io = {}) {
426
462
  };
427
463
  const spec = p.flags.get("--release") ?? "pinned";
428
464
  const m = releases2(opts);
465
+ const fresh = io.freshness === false || spec.trim() !== "pinned" || verb === undefined || !ANSWERING.has(verb) ? null : io.freshness ?? {};
466
+ const answered = { tag: null };
467
+ const open = async (s, o) => {
468
+ const rel = await openRelease2(s, o);
469
+ answered.tag = rel.tag;
470
+ return rel;
471
+ };
472
+ const now = fresh?.now ?? Date.now;
473
+ const started = now();
474
+ let check = null;
475
+ if (fresh !== null) {
476
+ try {
477
+ const repo = repoOf2(opts);
478
+ if (repo !== null)
479
+ check = latestCheck2(cacheRoot2(opts), repo, opts.remote ?? defaultRemote(), now).catch(() => null);
480
+ } catch {}
481
+ }
429
482
  const out = (v, text) => {
430
483
  if (json)
431
484
  say(JSON.stringify(v, null, 2));
@@ -477,6 +530,22 @@ async function main(argv, io = {}) {
477
530
  out(pin, () => say(`pinned ${pin.tag} (from ${pin.source})`));
478
531
  return 0;
479
532
  }
533
+ case "update":
534
+ case "use": {
535
+ const target = verb === "update" ? "latest" : rest[0];
536
+ if (target === undefined) {
537
+ log("use <spec>");
538
+ return 2;
539
+ }
540
+ const assets = (p.flags.get("--assets") ?? "results").split(",").filter((a) => a !== "");
541
+ const u = await m.use(target, { assets });
542
+ out(u, () => {
543
+ for (const b of u.built)
544
+ say(`${u.tag} ${b.name} ${b.path}`);
545
+ say(u.was === u.tag ? `pinned ${u.tag} (already pinned; its indexes are built)` : `pinned ${u.tag}${u.was === null ? "" : ` (was ${u.was})`}`);
546
+ });
547
+ return 0;
548
+ }
480
549
  case "unpin": {
481
550
  const was = m.unpin();
482
551
  out({ unpinned: was }, () => say(was ? "unpinned" : "no pin was set"));
@@ -484,6 +553,7 @@ async function main(argv, io = {}) {
484
553
  }
485
554
  case "current": {
486
555
  const c = m.current();
556
+ answered.tag = c.pin?.tag ?? null;
487
557
  out(c, () => say(c.pin === null ? "no pin: a bare open is latest" : `pinned ${c.pin.tag}${c.cached ? "" : " (not cached)"}`));
488
558
  return 0;
489
559
  }
@@ -506,7 +576,7 @@ async function main(argv, io = {}) {
506
576
  return 0;
507
577
  }
508
578
  case "capabilities": {
509
- const rel = await openRelease2(spec, { ...opts, assets: [] });
579
+ const rel = await open(spec, { ...opts, assets: [] });
510
580
  try {
511
581
  const c = await rel.capabilities();
512
582
  out(c, () => {
@@ -550,7 +620,7 @@ async function main(argv, io = {}) {
550
620
  return 0;
551
621
  }
552
622
  case "feis": {
553
- const rel = await openRelease2(spec, { ...opts, assets: [] });
623
+ const rel = await open(spec, { ...opts, assets: [] });
554
624
  const year = p.flags.get("--year");
555
625
  const a = await rel.feises.find({
556
626
  ...p.flags.has("--platform") ? { platform: p.flags.get("--platform") } : {},
@@ -574,7 +644,7 @@ async function main(argv, io = {}) {
574
644
  log("series <name|host code|feis id>");
575
645
  return 2;
576
646
  }
577
- const rel = await openRelease2(spec, { ...opts, assets: [] });
647
+ const rel = await open(spec, { ...opts, assets: [] });
578
648
  const a = await rel.feises.series(rest.join(" "));
579
649
  out(a, () => {
580
650
  say(`${a.arm} "${a.asked}" via ${a.via ?? "-"} in ${a.tag}${a.redirected !== undefined ? ` (${a.redirected.from} -> ${a.redirected.to})` : ""}${a.why !== undefined ? ` (${a.why})` : ""}`);
@@ -594,7 +664,7 @@ async function main(argv, io = {}) {
594
664
  log("school <print|school_id> [--region <code>]");
595
665
  return 2;
596
666
  }
597
- const rel = await openRelease2(spec, { ...opts, assets: [] });
667
+ const rel = await open(spec, { ...opts, assets: [] });
598
668
  const region = p.flags.get("--region");
599
669
  const a = await rel.schools.resolve(rest.join(" "), region !== undefined ? { region } : {});
600
670
  out(a, () => {
@@ -606,7 +676,7 @@ async function main(argv, io = {}) {
606
676
  return a.arm === "none" ? 1 : 0;
607
677
  }
608
678
  case "dancer": {
609
- const rel = await openRelease2(spec, { ...opts, assets: [] });
679
+ const rel = await open(spec, { ...opts, assets: [] });
610
680
  try {
611
681
  const id = p.flags.get("--id");
612
682
  if (id !== undefined) {
@@ -665,7 +735,7 @@ async function main(argv, io = {}) {
665
735
  }
666
736
  }
667
737
  case "results": {
668
- const rel = await openRelease2(spec, { ...opts, assets: [] });
738
+ const rel = await open(spec, { ...opts, assets: [] });
669
739
  try {
670
740
  const q = {
671
741
  ...p.flags.has("--dancer") ? { dancer_id: Number(p.flags.get("--dancer")) } : {},
@@ -683,7 +753,7 @@ async function main(argv, io = {}) {
683
753
  }
684
754
  }
685
755
  case "majors": {
686
- const rel = await openRelease2(spec, { ...opts, assets: [] });
756
+ const rel = await open(spec, { ...opts, assets: [] });
687
757
  try {
688
758
  const event = p.flags.get("--event");
689
759
  const year = p.flags.get("--year");
@@ -725,7 +795,7 @@ async function main(argv, io = {}) {
725
795
  }
726
796
  case "stream": {
727
797
  const q = streamQueryOf(p);
728
- const rel = await openRelease2(spec, { ...opts, assets: [] });
798
+ const rel = await open(spec, { ...opts, assets: [] });
729
799
  try {
730
800
  let n = 0;
731
801
  const rows = p.bools.has("--entries") ? rel.results.entries(q) : rel.results.stream(q);
@@ -740,7 +810,7 @@ async function main(argv, io = {}) {
740
810
  }
741
811
  }
742
812
  case "records": {
743
- const rel = await openRelease2(spec, { ...opts, assets: [] });
813
+ const rel = await open(spec, { ...opts, assets: [] });
744
814
  try {
745
815
  const ids = idsOf(p.flags.get("--dancer"));
746
816
  if (p.bools.has("--years")) {
@@ -768,7 +838,7 @@ async function main(argv, io = {}) {
768
838
  }
769
839
  }
770
840
  case "feis-dates": {
771
- const rel = await openRelease2(spec, { ...opts, assets: [] });
841
+ const rel = await open(spec, { ...opts, assets: [] });
772
842
  try {
773
843
  const a = await rel.feises.dates();
774
844
  out(a, () => {
@@ -782,7 +852,7 @@ async function main(argv, io = {}) {
782
852
  }
783
853
  }
784
854
  case "tabulated": {
785
- const rel = await openRelease2(spec, { ...opts, assets: [] });
855
+ const rel = await open(spec, { ...opts, assets: [] });
786
856
  try {
787
857
  const min = p.flags.get("--min-round");
788
858
  const a = await rel.majors.tabulated(min !== undefined ? { min_true_round: Number(min) } : {});
@@ -797,7 +867,7 @@ async function main(argv, io = {}) {
797
867
  }
798
868
  }
799
869
  case "teams": {
800
- const rel = await openRelease2(spec, { ...opts, assets: [] });
870
+ const rel = await open(spec, { ...opts, assets: [] });
801
871
  try {
802
872
  const q = eventYearOf(p);
803
873
  if (p.bools.has("--rosters")) {
@@ -833,7 +903,7 @@ async function main(argv, io = {}) {
833
903
  }
834
904
  }
835
905
  case "events": {
836
- const rel = await openRelease2(spec, { ...opts, assets: [] });
906
+ const rel = await open(spec, { ...opts, assets: [] });
837
907
  try {
838
908
  const a = await rel.events.years(eventYearOf(p));
839
909
  out(a, () => {
@@ -852,7 +922,7 @@ async function main(argv, io = {}) {
852
922
  log(`${verb} <name|TD number>`);
853
923
  return 2;
854
924
  }
855
- const rel = await openRelease2(spec, { ...opts, assets: [] });
925
+ const rel = await open(spec, { ...opts, assets: [] });
856
926
  try {
857
927
  const a = verb === "teacher" ? await rel.staff.teacher(rest.join(" ")) : await rel.staff.adjudicator(rest.join(" "));
858
928
  out(a, () => printStaff(a, say));
@@ -866,7 +936,7 @@ async function main(argv, io = {}) {
866
936
  log("comp <feis> <code> [--year <y>] [--title <t>]");
867
937
  return 2;
868
938
  }
869
- const rel = await openRelease2(spec, { ...opts, assets: [] });
939
+ const rel = await open(spec, { ...opts, assets: [] });
870
940
  const year = p.flags.get("--year");
871
941
  const a = await rel.comps.resolve({
872
942
  feis: rest[0],
@@ -882,7 +952,7 @@ async function main(argv, io = {}) {
882
952
  const of = p.flags.get("--for");
883
953
  if (of !== undefined) {
884
954
  const y = rest[0] ?? p.flags.get("--year");
885
- const rel = await openRelease2(spec, { ...opts, assets: [] });
955
+ const rel = await open(spec, { ...opts, assets: [] });
886
956
  const a = await rel.comps.syllabusFor(of, y !== undefined ? Number(y) : undefined);
887
957
  out(a, () => {
888
958
  say(`${a.arm} ${a.asked.series} asked ${a.asked.year ?? "-"} read ${a.year ?? "-"} (${a.held ? "held" : "not held"}) from ${a.source ?? "-"} series ${a.series_id ?? "-"} (years ${a.years.join(", ") || "-"}) in ${a.tag}`);
@@ -901,7 +971,7 @@ async function main(argv, io = {}) {
901
971
  log("syllabus <feis> [year]");
902
972
  return 2;
903
973
  }
904
- const rel = await openRelease2(spec, { ...opts, assets: [] });
974
+ const rel = await open(spec, { ...opts, assets: [] });
905
975
  const a = await rel.comps.syllabus(rest[0], rest[1] !== undefined ? Number(rest[1]) : undefined);
906
976
  out(a, () => {
907
977
  say(`${a.feis.asked} ${a.year ?? "?"} ${a.held ? `held: ${a.feis.feis_ids.join(", ")}` : "not held"} series ${a.feis.brand_id ?? "-"} (years ${a.years.join(", ") || "-"}) in ${a.tag}`);
@@ -913,7 +983,7 @@ async function main(argv, io = {}) {
913
983
  return a.held ? 0 : 1;
914
984
  }
915
985
  case "resolve": {
916
- const rel = await openRelease2(spec, { ...opts, assets: [] });
986
+ const rel = await open(spec, { ...opts, assets: [] });
917
987
  try {
918
988
  return await resolveVerb(rel, rest[0], rest.slice(1), p, say, io.stdin ?? (() => Bun.stdin.text()));
919
989
  } finally {
@@ -927,6 +997,14 @@ async function main(argv, io = {}) {
927
997
  } catch (e) {
928
998
  log(`idq ${verb}: ${e.message}`);
929
999
  return 1;
1000
+ } finally {
1001
+ if (fresh !== null && answered.tag !== null) {
1002
+ const budget = fresh.budgetMs ?? LATEST_CHECK_BUDGET_MS2;
1003
+ const c = check === null ? null : await within(check, budget - (now() - started));
1004
+ const w = staleWarning2(answered.tag, c?.latest ?? null, fresh.floor ?? null, c?.tags ?? [], fresh.command);
1005
+ if (w !== null)
1006
+ log(w);
1007
+ }
930
1008
  }
931
1009
  }
932
1010
  if (import.meta.main)
package/dist/index.js CHANGED
@@ -51,8 +51,14 @@ import {
51
51
  openRelease2,
52
52
  openCached2,
53
53
  ReleaseManager2,
54
- releases2
55
- } from "./chunks/index-6028k52f.js";
54
+ releases2,
55
+ LATEST_CHECK_MS2,
56
+ LATEST_CHECK_BUDGET_MS2,
57
+ latestCheckPath2,
58
+ readLatestCheck2,
59
+ latestCheck2,
60
+ staleWarning2
61
+ } from "./chunks/index-pfsxe80h.js";
56
62
  import {
57
63
  cacheRoot2,
58
64
  repoOf2
@@ -76,6 +82,8 @@ export {
76
82
  FEATURES2 as FEATURES,
77
83
  FEIS_LOCAL_ID_FLOOR2 as FEIS_LOCAL_ID_FLOOR,
78
84
  FeisFinder2 as FeisFinder,
85
+ LATEST_CHECK_BUDGET_MS2 as LATEST_CHECK_BUDGET_MS,
86
+ LATEST_CHECK_MS2 as LATEST_CHECK_MS,
79
87
  LINEAGE_FILE2 as LINEAGE_FILE,
80
88
  MajorsFinder2 as MajorsFinder,
81
89
  NAME_LEVELS2 as NAME_LEVELS,
@@ -105,6 +113,8 @@ export {
105
113
  indexPath2 as indexPath,
106
114
  initialsOnly2 as initialsOnly,
107
115
  isCurrent2 as isCurrent,
116
+ latestCheck2 as latestCheck,
117
+ latestCheckPath2 as latestCheckPath,
108
118
  linksOfLineage2 as linksOfLineage,
109
119
  listIndexes2 as listIndexes,
110
120
  nameLevel2 as nameLevel,
@@ -115,6 +125,7 @@ export {
115
125
  parseReleaseSpec2 as parseReleaseSpec,
116
126
  pickTag2 as pickTag,
117
127
  planAsset2 as planAsset,
128
+ readLatestCheck2 as readLatestCheck,
118
129
  readPin2 as readPin,
119
130
  readPopulation2 as readPopulation,
120
131
  readsSpanishNames2 as readsSpanishNames,
@@ -125,6 +136,7 @@ export {
125
136
  resolveTag2 as resolveTag,
126
137
  setRemote2 as setRemote,
127
138
  sha256File2 as sha256File,
139
+ staleWarning2 as staleWarning,
128
140
  syllabusSourceOf2 as syllabusSourceOf,
129
141
  unzstdTo2 as unzstdTo,
130
142
  youngest2 as youngest
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
+ import { type FreshnessFloor } from "./freshness.js";
2
3
  import type { OpenOptions } from "./release.js";
3
4
  import type { StreamQuery } from "./stream.js";
4
- export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]\n\n config repo <owner/name> set the user-local repository the releases come from\n list [--remote] the cached releases (size, files, indexes, the pin), and\n with --remote the published tags\n fetch [--release <spec>] [--assets results,majors,<file>...]\n download, verify and cache a release (default: results)\n pin <spec> the user-local pin: what a bare open (spec \"pinned\") opens\n unpin remove the pin (a bare open is then \"latest\")\n current the pin, and whether it is cached\n add <dir> --tag <tag> add a release built locally (its MANIFEST.json and files)\n to the cache under its tag\n index [--release <spec>] [<name>...] [--force]\n build the per-tag indexes (default: every one)\n verify [--release <spec>] re-hash every cached file against its receipt\n capabilities [--release <spec>] what the release carries of what the client reads, from\n its own schema: each feature full or missing (its calls\n refuse, naming the first cut that publishes it)\n prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)\n and the pinned one\n rm <tag> [--dry-run] remove one cached release (not the pinned one)\n drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)\n feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]\n the feises a query names\n series [--release <spec>] <name|host code|feis id>\n the series a name, a host code (fweb:ABC) or any feis id\n names: its years, hosts, codes and next instance\n school [--release <spec>] <print|school_id> [--region <code>]\n a school print or id to the published schools\n dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>\n [--order surname-first|given-first] [--school <print|id>]\n [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]\n [--population girls|boys] [--oireachtas <region>[:<year>]] [--rows]\n the dancers a name reaches, each with its evidence\n (name level, age, school, population, region) and the arm;\n --oireachtas is the regional the roster is for, its region in\n the release's spelling (SRO, MAO), its year the --as-of year\n where none is given; the results of\n the one dancer where one answers (or --rows for each)\n results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]\n [--school <print|id>] [--year <y>] [--limit <n>]\n the published result rows, prints and ids\n majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]\n the majors: every event and its years; an event's\n competitions; a competition's, an event's or a dancer's rows\n stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]\n [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]\n [--level <feis_level,...>] [--dancer <id,...>] [--school <school_id>] [--primary]\n every row a scope holds, one JSON line each, never the\n whole table in memory; --entries one per address (the row\n it stands for, with its recall order)\n records [--release <spec>] [--dancer <id,...>]\n every dancer's record: display, appearances, birth window,\n populations\n records --years [--release <spec>] [--dancer <id,...>] [--year <y>]\n each dancer's region, nationals and place per year\n feis-dates [--release <spec>] every feis's first and last day\n tabulated [--release <spec>] [--min-round <n>]\n the majors addresses the marks sheets tabulate to round n\n or later (default 3)\n teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]\n the team result rows of an event-year; with --rosters its\n roster teams and their dancers; with --joins how each roster\n team meets the results\n events [--release <spec>] [--event <e>] [--year <y>]\n each event-year's dates and place\n teacher [--release <spec>] <name|TD number>\n the staff lists' people: where they taught, the register,\n where they sat\n adjudicator [--release <spec>] <name|TD number>\n the register's and the panels' people: the same answer\n comp [--release <spec>] <feis> <code> [--year <y>] [--title <t>]\n a feis's own comp code to the comp it names, through that\n feis's syllabus (the prior year's only where the platform\n numbers every feis alike; elsewhere the title)\n syllabus [--release <spec>] <feis> [year]\n one feis-year's comps, in printed order\n syllabus [--release <spec>] --for <series> [year]\n a series' syllabus for a year: that year's where held,\n else the newest, saying which and what it is read from\n resolve comp [<feis> <code>] [--year <y>] [--title <t>]\n resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]\n [--order <o>] [--as-of <date>] [--population girls|boys]\n [--oireachtas <region>[:<year>]]\n resolve registrations\n JSONL out, one answer per line; with no positional the\n questions are JSONL on stdin, one per line (a comp:\n {feis, code, year?, title?}, a dancer: {name, school?, age?,\n population?, oireachtas?}, a registration: {name, school?,\n codes[], feis, year?, population?, oireachtas?})\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
5
+ export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]\n\n config repo <owner/name> set the user-local repository the releases come from\n list [--remote] the cached releases (size, files, indexes, the pin), and\n with --remote the published tags\n fetch [--release <spec>] [--assets results,majors,<file>...]\n download, verify and cache a release (default: results)\n update [--assets ...] move to the newest published release: fetch it (verified),\n build its indexes, then pin it\n use <spec> [--assets ...] the same for the release a spec names\n pin <spec> the user-local pin: what a bare open (spec \"pinned\") opens\n unpin remove the pin (a bare open is then \"latest\")\n current the pin, and whether it is cached\n add <dir> --tag <tag> add a release built locally (its MANIFEST.json and files)\n to the cache under its tag\n index [--release <spec>] [<name>...] [--force]\n build the per-tag indexes (default: every one)\n verify [--release <spec>] re-hash every cached file against its receipt\n capabilities [--release <spec>] what the release carries of what the client reads, from\n its own schema: each feature full or missing (its calls\n refuse, naming the first cut that publishes it)\n prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)\n and the pinned one\n rm <tag> [--dry-run] remove one cached release (not the pinned one)\n drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)\n feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]\n the feises a query names\n series [--release <spec>] <name|host code|feis id>\n the series a name, a host code (fweb:ABC) or any feis id\n names: its years, hosts, codes and next instance\n school [--release <spec>] <print|school_id> [--region <code>]\n a school print or id to the published schools\n dancer [--release <spec>] <name> | --surname <s> | --like <glob> | --id <dancer_id>\n [--order surname-first|given-first] [--school <print|id>]\n [--yob <y> | --band <U10> [--year <y>] | --comp <competition>] [--as-of <date>]\n [--population girls|boys] [--oireachtas <region>[:<year>]] [--rows]\n the dancers a name reaches, each with its evidence\n (name level, age, school, population, region) and the arm;\n --oireachtas is the regional the roster is for, its region in\n the release's spelling (SRO, MAO), its year the --as-of year\n where none is given; the results of\n the one dancer where one answers (or --rows for each)\n results [--release <spec>] [--dancer <id>] [--feis <id>] [--comp <competition>]\n [--school <print|id>] [--year <y>] [--limit <n>]\n the published result rows, prints and ids\n majors [--release <spec>] [--event <e>] [--year <y>] [--comp <competition>] [--dancer <id>]\n the majors: every event and its years; an event's\n competitions; a competition's, an event's or a dancer's rows\n stream [--release <spec>] --scope majors|transcribed|platform|all [--entries] [--event <e,...>]\n [--year <y> | --year <from>..<to>] [--tier <t,...>] [--category <c,...>]\n [--level <feis_level,...>] [--dancer <id,...>] [--school <school_id>] [--primary]\n every row a scope holds, one JSON line each, never the\n whole table in memory; --entries one per address (the row\n it stands for, with its recall order)\n records [--release <spec>] [--dancer <id,...>]\n every dancer's record: display, appearances, birth window,\n populations\n records --years [--release <spec>] [--dancer <id,...>] [--year <y>]\n each dancer's region, nationals and place per year\n feis-dates [--release <spec>] every feis's first and last day\n tabulated [--release <spec>] [--min-round <n>]\n the majors addresses the marks sheets tabulate to round n\n or later (default 3)\n teams [--release <spec>] [--event <e>] [--year <y>] [--rosters | --joins]\n the team result rows of an event-year; with --rosters its\n roster teams and their dancers; with --joins how each roster\n team meets the results\n events [--release <spec>] [--event <e>] [--year <y>]\n each event-year's dates and place\n teacher [--release <spec>] <name|TD number>\n the staff lists' people: where they taught, the register,\n where they sat\n adjudicator [--release <spec>] <name|TD number>\n the register's and the panels' people: the same answer\n comp [--release <spec>] <feis> <code> [--year <y>] [--title <t>]\n a feis's own comp code to the comp it names, through that\n feis's syllabus (the prior year's only where the platform\n numbers every feis alike; elsewhere the title)\n syllabus [--release <spec>] <feis> [year]\n one feis-year's comps, in printed order\n syllabus [--release <spec>] --for <series> [year]\n a series' syllabus for a year: that year's where held,\n else the newest, saying which and what it is read from\n resolve comp [<feis> <code>] [--year <y>] [--title <t>]\n resolve dancer [<name>] [--school <s>] [--comp <feis>/<code> | --yob <y> | --band <b> --year <y>]\n [--order <o>] [--as-of <date>] [--population girls|boys]\n [--oireachtas <region>[:<year>]]\n resolve registrations\n JSONL out, one answer per line; with no positional the\n questions are JSONL on stdin, one per line (a comp:\n {feis, code, year?, title?}, a dancer: {name, school?, age?,\n population?, oireachtas?}, a registration: {name, school?,\n codes[], feis, year?, population?, oireachtas?})\n\n A query on the default spec prints one warning line on stderr when its release is older than\n the newest published one (the remote asked at most once per hour, <cache>/LATEST.json); the\n answer and the exit code are unchanged, and an offline check says nothing.\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
5
6
  export interface Parsed {
6
7
  positional: string[];
7
8
  flags: Map<string, string>;
@@ -17,10 +18,17 @@ type Line<T> = {
17
18
  };
18
19
  export declare function jsonLines<T>(text: string, check: (v: T) => string | null): Line<T>[];
19
20
  export declare function streamQueryOf(p: Parsed): StreamQuery;
21
+ export interface FreshnessIO {
22
+ floor?: FreshnessFloor | null;
23
+ command?: string;
24
+ now?: () => number;
25
+ budgetMs?: number;
26
+ }
20
27
  export declare function main(argv: readonly string[], io?: {
21
28
  say?: (s: string) => void;
22
29
  log?: (s: string) => void;
23
30
  opts?: OpenOptions;
24
31
  stdin?: () => Promise<string>;
32
+ freshness?: FreshnessIO | false;
25
33
  }): Promise<number>;
26
34
  export {};
@@ -0,0 +1,18 @@
1
+ import type { Remote } from "./remote.js";
2
+ export declare const LATEST_CHECK_MS: number;
3
+ export declare const LATEST_CHECK_BUDGET_MS = 1500;
4
+ export interface LatestCheck {
5
+ repo: string;
6
+ checkedAt: string;
7
+ latest: string | null;
8
+ tags: string[];
9
+ }
10
+ export interface FreshnessFloor {
11
+ cut: number;
12
+ why: string;
13
+ }
14
+ export declare const latestCheckPath: (root: string) => string;
15
+ export declare function readLatestCheck(root: string): LatestCheck | null;
16
+ export declare function latestCheck(root: string, repo: string, remote: Remote, now?: () => number, intervalMs?: number): Promise<LatestCheck>;
17
+ export declare function within<T>(p: Promise<T>, ms: number): Promise<T | null>;
18
+ export declare function staleWarning(answered: string, latest: string | null, floor: FreshnessFloor | null, tags?: readonly string[], command?: string): string | null;
@@ -9,6 +9,7 @@ export { type Feis, type FeisAnswer, FeisFinder, type FeisQuery, NO_CALENDAR, ty
9
9
  export { type IndexBuilder, type IndexFile, buildIndex, defaultBuilders, ensureIndex, indexKey, indexPath, isCurrent, listIndexes, } from "./indexes.js";
10
10
  export { type CachedRelease, type ReleaseList, ReleaseManager, type RemovalPlan, releases, } from "./manager.js";
11
11
  export { type Registered, type Sat, type StaffAnswer, StaffFinder, type StaffPerson, type Taught, type TdLink, } from "./people.js";
12
+ export { type FreshnessFloor, LATEST_CHECK_BUDGET_MS, LATEST_CHECK_MS, type LatestCheck, latestCheck, latestCheckPath, readLatestCheck, staleWarning, } from "./freshness.js";
12
13
  export { type Pin, readPin } from "./pin.js";
13
14
  export { type Remote, type RemoteAsset, fallbackRemote, ghRemote, httpsRemote, setRemote, } from "./remote.js";
14
15
  export { type OpenOptions, Release, openCached, openRelease, resolveTag, } from "./release.js";
@@ -48,6 +48,17 @@ export declare class ReleaseManager {
48
48
  assets?: readonly string[];
49
49
  }): Promise<Release>;
50
50
  pin(spec: string | number): Promise<Pin>;
51
+ use(spec: string | number, o?: {
52
+ assets?: readonly string[];
53
+ }): Promise<{
54
+ tag: string;
55
+ was: string | null;
56
+ pin: Pin;
57
+ built: {
58
+ name: string;
59
+ path: string;
60
+ }[];
61
+ }>;
51
62
  unpin(): boolean;
52
63
  current(): {
53
64
  pin: Pin | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Open a versioned release of an Irish dance results archive: download, verify, decompress and cache it by tag, keep the cache, and resolve feises and schools against it",
5
5
  "keywords": ["irish-dance", "results", "release", "cache", "sqlite"],
6
6
  "license": "MIT",
@@ -44,8 +44,8 @@
44
44
  "prepublishOnly": "bun scripts/build.ts && bun ../../tools/checks/archive-client-tarball.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@finbheara/domain": "0.57.1",
48
- "@finbheara/names": "0.29.0",
47
+ "@finbheara/domain": "0.57.2",
48
+ "@finbheara/names": "0.30.0",
49
49
  "hyparquet": "1.31.2"
50
50
  },
51
51
  "devDependencies": {