@finbheara/archive-client 0.21.0 → 0.23.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
@@ -121,6 +121,7 @@ else `archive-client` in your home cache directory.
121
121
  <root>/<tag>/index/<name>.sqlite the per-tag indexes
122
122
  <root>/PINNED.json the user-local pin
123
123
  <root>/config.json the user-local config
124
+ <root>/local/<tag>/ a local build a default query answered from (below)
124
125
 
125
126
  A file already in place with no receipt (laid out by hand) is adopted where the manifest's hash
126
127
  vouches for it, and fetched again where it does not. A published release never changes, so a
@@ -139,13 +140,23 @@ m.unpin();
139
140
  await m.add("<dir>", "archive-YYYY.MM.DD-N"); // a release built locally, under its own tag
140
141
  await m.index("pinned"); // build every index
141
142
  m.prune({ keep: 2, dryRun: true }); // the plan: what would go, and its bytes
143
+ await m.use("latest"); // fetch, index, pin, then prune to the newest 2
142
144
  m.remove("<tag>"); // refuses the pinned release
143
145
  m.dropIndexes(); // the index files only
144
146
  await m.verify("<tag>"); // re-hash every cached file against its receipt
145
147
  ```
146
148
 
147
149
  Every removal answers its plan first (`dryRun`), keeps the pinned release, and touches nothing
148
- outside the cache root.
150
+ outside the cache root. A release's indexes live under its directory and go with it.
151
+
152
+ THE CACHE KEEPS ITSELF. A release is gigabytes, so a `use` (and so an `update`) that lands its
153
+ release prunes the cache once the pin has moved: it keeps the newest `keep` releases by tag (date,
154
+ cut, fraction), the new pin, and the pin before it, so a reader still on the previous release is
155
+ not cut off. A release fetched with `--release` and never pinned counts like any other. `keep` is
156
+ the call's own (`--keep N`, `use(spec, { keep })`), else the user-local `config keep <N>`, else
157
+ `DEFAULT_KEEP` (2), and a plain `prune` reads the same count. `--no-prune` (`prune: false`) skips
158
+ it, and a fetch or index that fails prunes nothing. The answer carries the plan (`pruned`), and
159
+ the command line prints one line per release removed and the MB freed.
149
160
 
150
161
  ## The indexes
151
162
 
@@ -707,10 +718,11 @@ is passed to the dancer resolver as asked (`rel.dancers`, above).
707
718
  ## The command line
708
719
 
709
720
  idq config repo <owner/name>
721
+ idq config keep <N>
710
722
  idq list [--remote]
711
723
  idq fetch [--release <spec>] [--assets results,majors,<file>...]
712
- idq update [--assets results,majors,<file>...]
713
- idq use <spec> [--assets results,majors,<file>...]
724
+ idq update [--assets results,majors,<file>...] [--keep N] [--no-prune]
725
+ idq use <spec> [--assets results,majors,<file>...] [--keep N] [--no-prune]
714
726
  idq pin <spec> | unpin | current
715
727
  idq add <dir> --tag <tag>
716
728
  idq index [--release <spec>] [<name>...] [--force]
@@ -752,7 +764,8 @@ is passed to the dancer resolver as asked (`rel.dancers`, above).
752
764
 
753
765
  `update` moves to the newest published release, as a package manager's `update` moves to the newest
754
766
  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
767
+ pins it, so a fetch or index that fails leaves the old pin in force, and then prunes the cache to
768
+ the newest releases (above: the pin and the one before it always stay). `use <spec>` does the same for
756
769
  the release a spec names, older or newer (`releases().use(spec)` in the API; `update` is
757
770
  `use("latest")`).
758
771
 
@@ -768,6 +781,29 @@ tag, a cut, `latest`) is never warned about: it was asked for. `main(argv, { fre
768
781
  `false` to turn the check off, or a `floor` (a cut the caller requires, and why: where only the
769
782
  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
783
 
784
+ A LOCAL BUILD ANSWERS THE DEFAULT SPEC. A project that builds releases can offer the client the
785
+ directories its unpublished builds sit in, `main(argv, { local: { builds } })`, where `builds()`
786
+ answers `{ dir, label, fraction? }[]`: a directory holding a build's `MANIFEST.json` and files as
787
+ its emitter wrote them, what to call it, and the fraction of its cut it builds. A query with no
788
+ `--release` then answers from the NEWEST COMPLETE build strictly newer than the pinned release
789
+ (the cut its manifest's input release names, then the fraction; with no pin, the newest cached
790
+ release; with neither, any complete build), and prints one line on stderr:
791
+
792
+ answered from <label> (unpublished, <dir>). Pass --release <tag> for a published one, or --live for the pin.
793
+
794
+ The answer's format and the exit code are otherwise the call's own. `--release <spec>` names a
795
+ release as ever, and `--live` forces the pinned one; neither reads a build. A build is COMPLETE
796
+ where its manifest parses, names an input release with a cut, and states a `results` database
797
+ that is on disk, each stated database at its stated bytes (`incomplete(dir)` says why not). The
798
+ caller leaves out a build it knows is still being written. A build is never read in place: its
799
+ files are added (hard-linked where the filesystem allows, every database checked against the
800
+ manifest's hash) to `<root>/local/<tag>` and indexed there on first use, the add made in a private
801
+ directory and renamed into place whole. `<root>/local` is no release tag, so `list`, `prune` and
802
+ `use` never see a build, the pin never moves to one, and it never counts among the releases a
803
+ prune keeps. The builds' cache keeps the one build it last answered from, and a build whose
804
+ manifest changed is added again. A verb whose index reads a file the build lacks (a table written
805
+ only when the release is published: `teams`, `events`) answers from the pinned release, saying so.
806
+
771
807
  `resolve` speaks JSON lines: one answer per line out, and with no positional question the
772
808
  questions are JSON lines on stdin, one per line (`{feis, code, year?, title?}`,
773
809
  `{name, school?, age?, population?, oireachtas?}`,
@@ -9,6 +9,8 @@ import {
9
9
  } from "./index-hne1wadw.js";
10
10
  import {
11
11
  parseArchiveTag2,
12
+ renderArchiveTag2,
13
+ cutOf2,
12
14
  compareTagsNewestFirst2,
13
15
  parseReleaseSpec2,
14
16
  pickTag2
@@ -2804,6 +2806,20 @@ function listIndexes2(rel) {
2804
2806
  });
2805
2807
  }
2806
2808
 
2809
+ import { createHash as createHash3 } from "crypto";
2810
+ import {
2811
+ copyFileSync as copyFileSync2,
2812
+ existsSync as existsSync6,
2813
+ linkSync as linkSync2,
2814
+ mkdirSync as mkdirSync5,
2815
+ readFileSync as readFileSync5,
2816
+ readdirSync as readdirSync4,
2817
+ renameSync as renameSync3,
2818
+ rmSync as rmSync5,
2819
+ statSync as statSync4
2820
+ } from "fs";
2821
+ import { join as join5 } from "path";
2822
+
2807
2823
  import {
2808
2824
  copyFileSync,
2809
2825
  existsSync as existsSync5,
@@ -3741,6 +3757,12 @@ function openCached2(tag, opts = {}) {
3741
3757
  return new Release2(tag, root, JSON.parse(readFileSync3(p, "utf8")), opts);
3742
3758
  }
3743
3759
 
3760
+ var DEFAULT_KEEP2 = 2;
3761
+ function checkKeep(keep) {
3762
+ if (!Number.isInteger(keep) || keep < 0)
3763
+ throw new Error(`prune: --keep ${keep} is not a count`);
3764
+ return keep;
3765
+ }
3744
3766
  function duBytes(p) {
3745
3767
  if (!existsSync5(p))
3746
3768
  return 0;
@@ -3801,13 +3823,24 @@ class ReleaseManager2 {
3801
3823
  return pin;
3802
3824
  }
3803
3825
  async use(spec, o = {}) {
3826
+ const keep = o.prune === false ? null : this.keepCount(o.keep);
3804
3827
  const was = readPin2(this.root)?.tag ?? null;
3805
3828
  const rel = await this.fetch(spec, o);
3806
3829
  const tag = rel.tag;
3807
3830
  rel.close();
3808
3831
  const { built } = await this.index(tag);
3809
3832
  const pin = await this.pin(tag);
3810
- return { tag, was, pin, built };
3833
+ if (keep === null)
3834
+ return { tag, was, pin, built, pruned: null };
3835
+ try {
3836
+ const hold = was === null || was === tag ? [] : [{ tag: was, why: "the pin before this one" }];
3837
+ return { tag, was, pin, built, pruned: this.prune({ keep, hold }) };
3838
+ } catch (e) {
3839
+ return { tag, was, pin, built, pruned: null, pruneError: e.message };
3840
+ }
3841
+ }
3842
+ keepCount(keep) {
3843
+ return checkKeep(keep ?? readConfig2(this.root).keep ?? DEFAULT_KEEP2);
3811
3844
  }
3812
3845
  unpin() {
3813
3846
  return clearPin(this.root);
@@ -3819,6 +3852,8 @@ class ReleaseManager2 {
3819
3852
  config(patch) {
3820
3853
  if (patch.repo !== undefined)
3821
3854
  repoOf2({ ...this.opts, repo: patch.repo });
3855
+ if (patch.keep !== undefined)
3856
+ checkKeep(patch.keep);
3822
3857
  return writeConfig2(this.root, patch);
3823
3858
  }
3824
3859
  readConfig() {
@@ -3896,16 +3931,17 @@ class ReleaseManager2 {
3896
3931
  return p;
3897
3932
  }
3898
3933
  prune(o = {}) {
3899
- const keep = o.keep ?? 1;
3900
- if (!Number.isInteger(keep) || keep < 0)
3901
- throw new Error(`prune: --keep ${keep} is not a count`);
3934
+ const keep = this.keepCount(o.keep);
3902
3935
  const pin = readPin2(this.root);
3903
3936
  const p = this.plan(o.dryRun ?? false);
3904
3937
  this.cachedTags().forEach((tag, i) => {
3938
+ const held = (o.hold ?? []).filter((h) => h.tag === tag).map((h) => h.why);
3905
3939
  if (tag === pin?.tag)
3906
3940
  p.keep.push({ tag, why: "pinned" });
3907
3941
  else if (i < keep)
3908
3942
  p.keep.push({ tag, why: `among the ${keep} newest` });
3943
+ else if (held.length > 0)
3944
+ p.keep.push({ tag, why: [...new Set(held)].join("; ") });
3909
3945
  else
3910
3946
  this.planRemove(p, tagDir(this.root, tag), `older than the ${keep} newest`);
3911
3947
  });
@@ -3946,17 +3982,178 @@ function releases2(opts = {}) {
3946
3982
  return new ReleaseManager2(opts);
3947
3983
  }
3948
3984
 
3949
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3950
- import { join as join5 } from "path";
3985
+ var LOCAL_DIR2 = "local";
3986
+ var localRoot2 = (root) => join5(root, LOCAL_DIR2);
3987
+ var newerThan2 = (a, b) => a.cut > b.cut || a.cut === b.cut && a.fraction > b.fraction;
3988
+ function readManifest(dir) {
3989
+ const p = join5(dir, "MANIFEST.json");
3990
+ if (!existsSync6(p))
3991
+ return null;
3992
+ try {
3993
+ const v = JSON.parse(readFileSync5(p, "utf8"));
3994
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
3995
+ } catch {
3996
+ return null;
3997
+ }
3998
+ }
3999
+ function incomplete2(dir) {
4000
+ const m = readManifest(dir);
4001
+ if (m === null)
4002
+ return "no MANIFEST.json that parses";
4003
+ if (cutOf2(typeof m.corpusRelease === "string" ? m.corpusRelease : undefined) === undefined)
4004
+ return "its manifest names no input release with a cut";
4005
+ const dbs = m.databases ?? {};
4006
+ if (dbs.results === undefined)
4007
+ return "its manifest states no results database";
4008
+ for (const name of DATABASES2) {
4009
+ const d = dbs[name];
4010
+ if (d === undefined)
4011
+ continue;
4012
+ const p = join5(dir, d.file ?? `${name}.sqlite`);
4013
+ if (!existsSync6(p))
4014
+ return `no ${d.file ?? `${name}.sqlite`}`;
4015
+ if (d.bytes !== undefined && statSync4(p).size !== d.bytes)
4016
+ return `${d.file ?? `${name}.sqlite`} is ${statSync4(p).size} bytes, the manifest says ${d.bytes}`;
4017
+ }
4018
+ return null;
4019
+ }
4020
+ function releaseVersion2(rel) {
4021
+ const t = parseArchiveTag2(rel.tag);
4022
+ const cut = cutOf2(typeof rel.manifest.corpusRelease === "string" ? rel.manifest.corpusRelease : undefined) ?? t?.cut;
4023
+ if (cut === undefined)
4024
+ return null;
4025
+ const f = rel.manifest.archiveFraction;
4026
+ return { cut, fraction: typeof f === "number" ? f : t?.fraction ?? 0 };
4027
+ }
4028
+ function pickBuild2(builds, base) {
4029
+ let best = null;
4030
+ for (const b of builds) {
4031
+ if (incomplete2(b.dir) !== null)
4032
+ continue;
4033
+ const manifest = readManifest(b.dir);
4034
+ const version = {
4035
+ cut: cutOf2(manifest.corpusRelease),
4036
+ fraction: b.fraction ?? 0
4037
+ };
4038
+ if (base !== null && !newerThan2(version, base))
4039
+ continue;
4040
+ if (best === null || newerThan2(version, best.version))
4041
+ best = { build: b, version, manifest };
4042
+ }
4043
+ return best;
4044
+ }
4045
+ function buildTag2(p) {
4046
+ const at = typeof p.manifest.emittedAt === "string" && !Number.isNaN(Date.parse(p.manifest.emittedAt)) ? new Date(p.manifest.emittedAt) : statSync4(join5(p.build.dir, "MANIFEST.json")).mtime;
4047
+ const date = at.toISOString().slice(0, 10).replaceAll("-", ".");
4048
+ return renderArchiveTag2({ date, cut: p.version.cut, fraction: p.version.fraction });
4049
+ }
4050
+ function buildFiles(dir) {
4051
+ return readdirSync4(dir).filter((n) => statSync4(join5(dir, n)).isFile()).filter((n) => {
4052
+ const m = /^(.*)\.sqlite(\.zst)?$/.exec(n);
4053
+ return m === null || isDatabase(m[1]);
4054
+ }).sort();
4055
+ }
4056
+ function lacks(b, manifest, inputs) {
4057
+ return inputs.filter((a) => !planAsset2(a, manifest).published.some((n) => existsSync6(join5(b.dir, n))));
4058
+ }
4059
+ function buildRemote(label) {
4060
+ const refuse = async () => {
4061
+ throw new Error(`archive-client: ${label} is a local build: it carries only its own files`);
4062
+ };
4063
+ return { name: "local-build", tags: refuse, assets: refuse, download: refuse };
4064
+ }
4065
+ var sha256Of = (path) => createHash3("sha256").update(readFileSync5(path)).digest("hex");
4066
+ var scratchName = (kind) => `.${kind}-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
4067
+ function buildOptions(root, label, opts) {
4068
+ return { ...opts, cache: localRoot2(root), remote: buildRemote(label), assets: [] };
4069
+ }
4070
+ async function openBuild2(root, p, opts) {
4071
+ const o = buildOptions(root, p.build.label, opts);
4072
+ const local = localRoot2(root);
4073
+ const tag = buildTag2(p);
4074
+ const want = sha256Of(join5(p.build.dir, "MANIFEST.json"));
4075
+ const current = () => {
4076
+ const rel = openCached2(tag, o);
4077
+ return rel !== null && rel.receipt().files["MANIFEST.json"]?.sha256 === want ? rel : null;
4078
+ };
4079
+ const have = current();
4080
+ if (have !== null)
4081
+ return have;
4082
+ mkdirSync5(local, { recursive: true });
4083
+ const work = join5(local, scratchName("add"));
4084
+ try {
4085
+ const stage = join5(work, "stage");
4086
+ mkdirSync5(stage, { recursive: true });
4087
+ for (const f of buildFiles(p.build.dir)) {
4088
+ try {
4089
+ linkSync2(join5(p.build.dir, f), join5(stage, f));
4090
+ } catch {
4091
+ copyFileSync2(join5(p.build.dir, f), join5(stage, f));
4092
+ }
4093
+ }
4094
+ const added = await new ReleaseManager2({ ...o, cache: join5(work, "cache") }).add(stage, tag);
4095
+ const receipt = added.receipt();
4096
+ receipt.source = `local:${p.build.dir}`;
4097
+ writeJsonAtomic2(receiptPath(added.dir), receipt);
4098
+ added.close();
4099
+ const dest = join5(local, tag);
4100
+ if (existsSync6(dest) && current() === null) {
4101
+ try {
4102
+ renameSync3(dest, join5(local, scratchName("old")));
4103
+ } catch {}
4104
+ }
4105
+ try {
4106
+ renameSync3(added.dir, dest);
4107
+ } catch {}
4108
+ } finally {
4109
+ rmSync5(work, { recursive: true, force: true });
4110
+ }
4111
+ const rel = current();
4112
+ if (rel === null)
4113
+ throw new Error(`archive-client: ${p.build.label} did not land in ${local}`);
4114
+ return rel;
4115
+ }
4116
+ function alive(pid) {
4117
+ try {
4118
+ process.kill(pid, 0);
4119
+ return true;
4120
+ } catch (e) {
4121
+ return e.code === "EPERM";
4122
+ }
4123
+ }
4124
+ function sweepBuilds2(root, keep) {
4125
+ const local = localRoot2(root);
4126
+ if (!existsSync6(local))
4127
+ return [];
4128
+ const gone = [];
4129
+ for (const name of readdirSync4(local).sort()) {
4130
+ if (name === keep)
4131
+ continue;
4132
+ const m = /^\.(add|old)-(\d+)-/.exec(name);
4133
+ if (m !== null && m[1] === "add" && alive(Number(m[2])))
4134
+ continue;
4135
+ if (m === null && parseArchiveTag2(name) === null)
4136
+ continue;
4137
+ const path = join5(local, name);
4138
+ try {
4139
+ rmSync5(path, { recursive: true, force: true });
4140
+ gone.push(path);
4141
+ } catch {}
4142
+ }
4143
+ return gone;
4144
+ }
4145
+
4146
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
4147
+ import { join as join6 } from "path";
3951
4148
  var LATEST_CHECK_MS2 = 60 * 60 * 1000;
3952
4149
  var LATEST_CHECK_BUDGET_MS2 = 1500;
3953
- var latestCheckPath2 = (root) => join5(root, "LATEST.json");
4150
+ var latestCheckPath2 = (root) => join6(root, "LATEST.json");
3954
4151
  function readLatestCheck2(root) {
3955
4152
  const p = latestCheckPath2(root);
3956
- if (!existsSync6(p))
4153
+ if (!existsSync7(p))
3957
4154
  return null;
3958
4155
  try {
3959
- const v = JSON.parse(readFileSync5(p, "utf8"));
4156
+ const v = JSON.parse(readFileSync6(p, "utf8"));
3960
4157
  if (typeof v.repo !== "string" || typeof v.checkedAt !== "string")
3961
4158
  return null;
3962
4159
  return {
@@ -4035,4 +4232,4 @@ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
4035
4232
  return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
4036
4233
  }
4037
4234
 
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 };
4235
+ 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, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2 };
package/dist/cli.js CHANGED
@@ -6,26 +6,39 @@ import {
6
6
  } from "./chunks/index-hne1wadw.js";
7
7
  import {
8
8
  nameOrderOf,
9
+ defaultBuilders2,
10
+ readPin2,
9
11
  defaultRemote,
10
12
  openRelease2,
13
+ openCached2,
11
14
  releases2,
15
+ releaseVersion2,
16
+ pickBuild2,
17
+ lacks,
18
+ openBuild2,
19
+ sweepBuilds2,
12
20
  LATEST_CHECK_BUDGET_MS2,
13
21
  latestCheck2,
14
22
  within,
15
23
  staleWarning2
16
- } from "./chunks/index-pfsxe80h.js";
24
+ } from "./chunks/index-vsk6837c.js";
17
25
  import"./chunks/index-hghdy980.js";
18
26
 
19
- var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
27
+ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json] [--live]
20
28
 
21
29
  config repo <owner/name> set the user-local repository the releases come from
30
+ config keep <N> how many of the newest releases a prune keeps (default 2)
22
31
  list [--remote] the cached releases (size, files, indexes, the pin), and
23
32
  with --remote the published tags
24
33
  fetch [--release <spec>] [--assets results,majors,<file>...]
25
34
  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
35
+ update [--assets ...] [--keep N] [--no-prune]
36
+ move to the newest published release: fetch it (verified),
37
+ build its indexes, pin it, then prune the cache to the N
38
+ newest releases (default: config keep, else 2), keeping the
39
+ new pin and the one before it
40
+ use <spec> [--assets ...] [--keep N] [--no-prune]
41
+ the same for the release a spec names
29
42
  pin <spec> the user-local pin: what a bare open (spec "pinned") opens
30
43
  unpin remove the pin (a bare open is then "latest")
31
44
  current the pin, and whether it is cached
@@ -37,8 +50,8 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
37
50
  capabilities [--release <spec>] what the release carries of what the client reads, from
38
51
  its own schema: each feature full or missing (its calls
39
52
  refuse, naming the first cut that publishes it)
40
- prune [--keep N] [--dry-run] remove all but the N newest cached releases (default 1)
41
- and the pinned one
53
+ prune [--keep N] [--dry-run] remove all but the N newest cached releases (default:
54
+ config keep, else 2) and the pinned one
42
55
  rm <tag> [--dry-run] remove one cached release (not the pinned one)
43
56
  drop-indexes [<tag>] [--dry-run] remove the index files (of one release, or of every one)
44
57
  feis [--release <spec>] [--platform <p>] [--code <c>] [--id <id>] [--name <glob>] [--year <y>]
@@ -114,6 +127,10 @@ var USAGE = `usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json]
114
127
  the newest published one (the remote asked at most once per hour, <cache>/LATEST.json); the
115
128
  answer and the exit code are unchanged, and an offline check says nothing.
116
129
 
130
+ Where the caller names local builds (a wrapper's option), a query with no --release answers
131
+ from the newest COMPLETE build newer than the pinned release, and says so in one line on
132
+ stderr; --release <spec> names a release, and --live forces the pinned one.
133
+
117
134
  <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)`;
118
135
  var VALUE = new Set([
119
136
  "--cache",
@@ -177,6 +194,9 @@ var mb = (n) => `${(n / 1e6).toFixed(1)} MB`;
177
194
  function printPlan(p, say) {
178
195
  for (const k of p.keep)
179
196
  say(`keep ${k.tag} (${k.why})`);
197
+ printRemovals(p, say);
198
+ }
199
+ function printRemovals(p, say) {
180
200
  for (const r of p.remove)
181
201
  say(`${p.dryRun ? "would remove" : "removed"} ${r.path} ${mb(r.bytes)} (${r.why})`);
182
202
  say(`${p.dryRun ? "would free" : "freed"} ${mb(p.bytes)}${p.dryRun ? " (dry run: nothing removed)" : ""}`);
@@ -441,6 +461,10 @@ var ANSWERING = new Set([
441
461
  "syllabus",
442
462
  "resolve"
443
463
  ]);
464
+ var VERB_INDEXES = {
465
+ teams: ["teams"],
466
+ events: ["events"]
467
+ };
444
468
  async function main(argv, io = {}) {
445
469
  const say = io.say ?? ((s) => console.log(s));
446
470
  const log = io.log ?? ((s) => console.error(s));
@@ -463,9 +487,43 @@ async function main(argv, io = {}) {
463
487
  const spec = p.flags.get("--release") ?? "pinned";
464
488
  const m = releases2(opts);
465
489
  const fresh = io.freshness === false || spec.trim() !== "pinned" || verb === undefined || !ANSWERING.has(verb) ? null : io.freshness ?? {};
466
- const answered = { tag: null };
490
+ const answered = { tag: null, build: null };
491
+ const local = io.local === undefined || io.local === false || p.flags.has("--release") || p.bools.has("--live") || verb === undefined || verb === "current" || !ANSWERING.has(verb) ? null : io.local;
492
+ const fromBuild = async (o) => {
493
+ if (local === null)
494
+ return null;
495
+ const root = cacheRoot2(o);
496
+ const builds = local.builds();
497
+ const pin = readPin2(root);
498
+ const baseTag = pin?.tag ?? m.cachedTags()[0] ?? null;
499
+ const baseRel = baseTag === null ? null : openCached2(baseTag, o);
500
+ const base = baseTag === null ? null : releaseVersion2(baseRel ?? { tag: baseTag, manifest: {} });
501
+ baseRel?.close();
502
+ const picked = builds.length === 0 ? null : pickBuild2(builds, base);
503
+ if (picked === null) {
504
+ sweepBuilds2(root, null);
505
+ return null;
506
+ }
507
+ const known = o.builders ?? defaultBuilders2();
508
+ const inputs = (VERB_INDEXES[verb] ?? []).flatMap((n) => known.find((x) => x.name === n)?.inputs ?? []);
509
+ const missing = lacks(picked.build, picked.manifest, inputs);
510
+ if (missing.length > 0) {
511
+ log(`${picked.build.label} (unpublished, ${picked.build.dir}) carries no ${missing.join(", ")}: answered from the published release`);
512
+ return null;
513
+ }
514
+ let rel;
515
+ try {
516
+ rel = await openBuild2(root, picked, o);
517
+ } catch (e) {
518
+ log(`${picked.build.label} (unpublished, ${picked.build.dir}) could not be read (${e.message}): answered from the published release`);
519
+ return null;
520
+ }
521
+ sweepBuilds2(root, rel.tag);
522
+ answered.build = picked;
523
+ return rel;
524
+ };
467
525
  const open = async (s, o) => {
468
- const rel = await openRelease2(s, o);
526
+ const rel = await fromBuild(o) ?? await openRelease2(s, o);
469
527
  answered.tag = rel.tag;
470
528
  return rel;
471
529
  };
@@ -488,12 +546,12 @@ async function main(argv, io = {}) {
488
546
  try {
489
547
  switch (verb) {
490
548
  case "config": {
491
- if (rest[0] !== "repo" || rest[1] === undefined) {
549
+ if (rest[0] !== "repo" && rest[0] !== "keep" || rest[1] === undefined) {
492
550
  out(m.readConfig(), () => say(JSON.stringify(m.readConfig())));
493
551
  return rest.length === 0 ? 0 : 2;
494
552
  }
495
- const c = m.config({ repo: rest[1] });
496
- out(c, () => say(`repo ${c.repo}`));
553
+ const c = rest[0] === "repo" ? m.config({ repo: rest[1] }) : m.config({ keep: Number(rest[1]) });
554
+ out(c, () => say(rest[0] === "repo" ? `repo ${c.repo}` : `keep ${c.keep}`));
497
555
  return 0;
498
556
  }
499
557
  case "list": {
@@ -538,12 +596,21 @@ async function main(argv, io = {}) {
538
596
  return 2;
539
597
  }
540
598
  const assets = (p.flags.get("--assets") ?? "results").split(",").filter((a) => a !== "");
541
- const u = await m.use(target, { assets });
599
+ const keep = p.flags.has("--keep") ? Number(p.flags.get("--keep")) : undefined;
600
+ const u = await m.use(target, {
601
+ assets,
602
+ prune: !p.bools.has("--no-prune"),
603
+ ...keep !== undefined ? { keep } : {}
604
+ });
542
605
  out(u, () => {
543
606
  for (const b of u.built)
544
607
  say(`${u.tag} ${b.name} ${b.path}`);
545
608
  say(u.was === u.tag ? `pinned ${u.tag} (already pinned; its indexes are built)` : `pinned ${u.tag}${u.was === null ? "" : ` (was ${u.was})`}`);
609
+ if (u.pruned !== null && u.pruned.remove.length > 0)
610
+ printRemovals(u.pruned, say);
546
611
  });
612
+ if (u.pruneError !== undefined)
613
+ log(`prune after ${verb} failed: ${u.pruneError}`);
547
614
  return 0;
548
615
  }
549
616
  case "unpin": {
@@ -998,12 +1065,15 @@ async function main(argv, io = {}) {
998
1065
  log(`idq ${verb}: ${e.message}`);
999
1066
  return 1;
1000
1067
  } finally {
1068
+ const b = answered.build;
1069
+ if (b !== null)
1070
+ log(`answered from ${b.build.label} (unpublished, ${b.build.dir}). Pass --release <tag> for a published one, or --live for the pin.`);
1001
1071
  if (fresh !== null && answered.tag !== null) {
1002
1072
  const budget = fresh.budgetMs ?? LATEST_CHECK_BUDGET_MS2;
1003
1073
  const c = check === null ? null : await within(check, budget - (now() - started));
1004
1074
  const w = staleWarning2(answered.tag, c?.latest ?? null, fresh.floor ?? null, c?.tags ?? [], fresh.command);
1005
1075
  if (w !== null)
1006
- log(w);
1076
+ log(b === null ? w : w.replace(`answered from ${answered.tag}.`, `answered from ${b.build.label}.`));
1007
1077
  }
1008
1078
  }
1009
1079
  }
package/dist/index.js CHANGED
@@ -50,15 +50,25 @@ import {
50
50
  Release2,
51
51
  openRelease2,
52
52
  openCached2,
53
+ DEFAULT_KEEP2,
53
54
  ReleaseManager2,
54
55
  releases2,
56
+ LOCAL_DIR2,
57
+ localRoot2,
58
+ newerThan2,
59
+ incomplete2,
60
+ releaseVersion2,
61
+ pickBuild2,
62
+ buildTag2,
63
+ openBuild2,
64
+ sweepBuilds2,
55
65
  LATEST_CHECK_MS2,
56
66
  LATEST_CHECK_BUDGET_MS2,
57
67
  latestCheckPath2,
58
68
  readLatestCheck2,
59
69
  latestCheck2,
60
70
  staleWarning2
61
- } from "./chunks/index-pfsxe80h.js";
71
+ } from "./chunks/index-vsk6837c.js";
62
72
  import {
63
73
  cacheRoot2,
64
74
  repoOf2
@@ -77,6 +87,7 @@ export {
77
87
  ARCHIVE_TAG2 as ARCHIVE_TAG,
78
88
  CompResolver2 as CompResolver,
79
89
  DATABASES2 as DATABASES,
90
+ DEFAULT_KEEP2 as DEFAULT_KEEP,
80
91
  DancerResolver2 as DancerResolver,
81
92
  EventFinder2 as EventFinder,
82
93
  FEATURES2 as FEATURES,
@@ -85,6 +96,7 @@ export {
85
96
  LATEST_CHECK_BUDGET_MS2 as LATEST_CHECK_BUDGET_MS,
86
97
  LATEST_CHECK_MS2 as LATEST_CHECK_MS,
87
98
  LINEAGE_FILE2 as LINEAGE_FILE,
99
+ LOCAL_DIR2 as LOCAL_DIR,
88
100
  MajorsFinder2 as MajorsFinder,
89
101
  NAME_LEVELS2 as NAME_LEVELS,
90
102
  NO_CALENDAR2 as NO_CALENDAR,
@@ -96,6 +108,7 @@ export {
96
108
  StaffFinder2 as StaffFinder,
97
109
  TeamFinder2 as TeamFinder,
98
110
  buildIndex2 as buildIndex,
111
+ buildTag2 as buildTag,
99
112
  cacheRoot2 as cacheRoot,
100
113
  canonicalOf2 as canonicalOf,
101
114
  capabilityOf2 as capabilityOf,
@@ -109,6 +122,7 @@ export {
109
122
  fallbackRemote2 as fallbackRemote,
110
123
  ghRemote2 as ghRemote,
111
124
  httpsRemote2 as httpsRemote,
125
+ incomplete2 as incomplete,
112
126
  indexKey2 as indexKey,
113
127
  indexPath2 as indexPath,
114
128
  initialsOnly2 as initialsOnly,
@@ -117,12 +131,16 @@ export {
117
131
  latestCheckPath2 as latestCheckPath,
118
132
  linksOfLineage2 as linksOfLineage,
119
133
  listIndexes2 as listIndexes,
134
+ localRoot2 as localRoot,
120
135
  nameLevel2 as nameLevel,
121
136
  needsRelease2 as needsRelease,
137
+ newerThan2 as newerThan,
138
+ openBuild2 as openBuild,
122
139
  openCached2 as openCached,
123
140
  openRelease2 as openRelease,
124
141
  parseArchiveTag2 as parseArchiveTag,
125
142
  parseReleaseSpec2 as parseReleaseSpec,
143
+ pickBuild2 as pickBuild,
126
144
  pickTag2 as pickTag,
127
145
  planAsset2 as planAsset,
128
146
  readLatestCheck2 as readLatestCheck,
@@ -130,6 +148,7 @@ export {
130
148
  readPopulation2 as readPopulation,
131
149
  readsSpanishNames2 as readsSpanishNames,
132
150
  releaseSchoolGraph2 as releaseSchoolGraph,
151
+ releaseVersion2 as releaseVersion,
133
152
  releases2 as releases,
134
153
  renderArchiveTag2 as renderArchiveTag,
135
154
  repoOf2 as repoOf,
@@ -137,6 +156,7 @@ export {
137
156
  setRemote2 as setRemote,
138
157
  sha256File2 as sha256File,
139
158
  staleWarning2 as staleWarning,
159
+ sweepBuilds2 as sweepBuilds,
140
160
  syllabusSourceOf2 as syllabusSourceOf,
141
161
  unzstdTo2 as unzstdTo,
142
162
  youngest2 as youngest
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  import { type FreshnessFloor } from "./freshness.js";
3
+ import { type LocalBuild } from "./local.js";
3
4
  import type { OpenOptions } from "./release.js";
4
5
  import type { StreamQuery } from "./stream.js";
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)";
6
+ export declare const USAGE = "usage: idq <verb> [--cache <dir>] [--repo <owner/name>] [--json] [--live]\n\n config repo <owner/name> set the user-local repository the releases come from\n config keep <N> how many of the newest releases a prune keeps (default 2)\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 ...] [--keep N] [--no-prune]\n move to the newest published release: fetch it (verified),\n build its indexes, pin it, then prune the cache to the N\n newest releases (default: config keep, else 2), keeping the\n new pin and the one before it\n use <spec> [--assets ...] [--keep N] [--no-prune]\n 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:\n config keep, else 2) 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 Where the caller names local builds (a wrapper's option), a query with no --release answers\n from the newest COMPLETE build newer than the pinned release, and says so in one line on\n stderr; --release <spec> names a release, and --live forces the pinned one.\n\n <spec>: pinned (the default) | latest | a cut (42, 42.1) | a tag (archive-YYYY.MM.DD-N)";
6
7
  export interface Parsed {
7
8
  positional: string[];
8
9
  flags: Map<string, string>;
@@ -18,6 +19,9 @@ type Line<T> = {
18
19
  };
19
20
  export declare function jsonLines<T>(text: string, check: (v: T) => string | null): Line<T>[];
20
21
  export declare function streamQueryOf(p: Parsed): StreamQuery;
22
+ export interface LocalIO {
23
+ builds: () => readonly LocalBuild[];
24
+ }
21
25
  export interface FreshnessIO {
22
26
  floor?: FreshnessFloor | null;
23
27
  command?: string;
@@ -30,5 +34,6 @@ export declare function main(argv: readonly string[], io?: {
30
34
  opts?: OpenOptions;
31
35
  stdin?: () => Promise<string>;
32
36
  freshness?: FreshnessIO | false;
37
+ local?: LocalIO | false;
33
38
  }): Promise<number>;
34
39
  export {};
@@ -7,6 +7,7 @@ export declare function expandHome(p: string): string;
7
7
  export declare function cacheRoot(opts?: ClientOptions): string;
8
8
  export interface UserConfig {
9
9
  repo?: string;
10
+ keep?: number;
10
11
  }
11
12
  export declare const configPath: (root: string) => string;
12
13
  export declare function readConfig(root: string): UserConfig;
@@ -7,7 +7,8 @@ export { type Comp, type CompAnswer, type CompDance, type CompKind, type CompQue
7
7
  export { type ClientOptions, cacheRoot, repoOf } from "./config.js";
8
8
  export { type Feis, type FeisAnswer, FeisFinder, type FeisQuery, NO_CALENDAR, type Series, type SeriesAnswer, type SeriesInstance, type SeriesNext, } from "./feises.js";
9
9
  export { type IndexBuilder, type IndexFile, buildIndex, defaultBuilders, ensureIndex, indexKey, indexPath, isCurrent, listIndexes, } from "./indexes.js";
10
- export { type CachedRelease, type ReleaseList, ReleaseManager, type RemovalPlan, releases, } from "./manager.js";
10
+ export { type CutVersion, LOCAL_DIR, type LocalBuild, type PickedBuild, buildTag, incomplete, localRoot, newerThan, openBuild, pickBuild, releaseVersion, sweepBuilds, } from "./local.js";
11
+ export { type CachedRelease, DEFAULT_KEEP, type ReleaseList, ReleaseManager, type RemovalPlan, type UseResult, releases, } from "./manager.js";
11
12
  export { type Registered, type Sat, type StaffAnswer, StaffFinder, type StaffPerson, type Taught, type TdLink, } from "./people.js";
12
13
  export { type FreshnessFloor, LATEST_CHECK_BUDGET_MS, LATEST_CHECK_MS, type LatestCheck, latestCheck, latestCheckPath, readLatestCheck, staleWarning, } from "./freshness.js";
13
14
  export { type Pin, readPin } from "./pin.js";
@@ -0,0 +1,33 @@
1
+ import { type ReleaseManifest } from "./assets.js";
2
+ import { type OpenOptions, type Release } from "./release.js";
3
+ import type { Remote } from "./remote.js";
4
+ export interface LocalBuild {
5
+ dir: string;
6
+ label: string;
7
+ fraction?: number;
8
+ }
9
+ export interface CutVersion {
10
+ cut: number;
11
+ fraction: number;
12
+ }
13
+ export declare const LOCAL_DIR = "local";
14
+ export declare const localRoot: (root: string) => string;
15
+ export declare const newerThan: (a: CutVersion, b: CutVersion) => boolean;
16
+ export declare function incomplete(dir: string): string | null;
17
+ export declare function releaseVersion(rel: {
18
+ tag: string;
19
+ manifest: ReleaseManifest;
20
+ }): CutVersion | null;
21
+ export interface PickedBuild {
22
+ build: LocalBuild;
23
+ version: CutVersion;
24
+ manifest: ReleaseManifest;
25
+ }
26
+ export declare function pickBuild(builds: readonly LocalBuild[], base: CutVersion | null): PickedBuild | null;
27
+ export declare function buildTag(p: PickedBuild): string;
28
+ export declare function buildFiles(dir: string): string[];
29
+ export declare function lacks(b: LocalBuild, manifest: ReleaseManifest, inputs: readonly string[]): string[];
30
+ export declare function buildRemote(label: string): Remote;
31
+ export declare function buildOptions(root: string, label: string, opts: OpenOptions): OpenOptions;
32
+ export declare function openBuild(root: string, p: PickedBuild, opts: OpenOptions): Promise<Release>;
33
+ export declare function sweepBuilds(root: string, keep: string | null): string[];
@@ -1,4 +1,5 @@
1
1
  import { type LandedFile } from "./assets.js";
2
+ import { type UserConfig } from "./config.js";
2
3
  import { type IndexFile } from "./indexes.js";
3
4
  import { type Pin } from "./pin.js";
4
5
  import { type OpenOptions, type Release, resolveTag } from "./release.js";
@@ -35,6 +36,18 @@ export interface RemovalPlan {
35
36
  }[];
36
37
  bytes: number;
37
38
  }
39
+ export declare const DEFAULT_KEEP = 2;
40
+ export interface UseResult {
41
+ tag: string;
42
+ was: string | null;
43
+ pin: Pin;
44
+ built: {
45
+ name: string;
46
+ path: string;
47
+ }[];
48
+ pruned: RemovalPlan | null;
49
+ pruneError?: string;
50
+ }
38
51
  export declare class ReleaseManager {
39
52
  private readonly opts;
40
53
  readonly root: string;
@@ -50,28 +63,17 @@ export declare class ReleaseManager {
50
63
  pin(spec: string | number): Promise<Pin>;
51
64
  use(spec: string | number, o?: {
52
65
  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
- }>;
66
+ prune?: boolean;
67
+ keep?: number;
68
+ }): Promise<UseResult>;
69
+ keepCount(keep?: number): number;
62
70
  unpin(): boolean;
63
71
  current(): {
64
72
  pin: Pin | null;
65
73
  cached: boolean;
66
74
  };
67
- config(patch: {
68
- repo?: string;
69
- }): {
70
- repo?: string;
71
- };
72
- readConfig(): {
73
- repo?: string;
74
- };
75
+ config(patch: UserConfig): UserConfig;
76
+ readConfig(): UserConfig;
75
77
  add(dir: string, tag: string): Promise<Release>;
76
78
  index(spec: string | number, names?: readonly string[], o?: {
77
79
  force?: boolean;
@@ -88,6 +90,10 @@ export declare class ReleaseManager {
88
90
  prune(o?: {
89
91
  keep?: number;
90
92
  dryRun?: boolean;
93
+ hold?: readonly {
94
+ tag: string;
95
+ why: string;
96
+ }[];
91
97
  }): RemovalPlan;
92
98
  remove(tag: string, o?: {
93
99
  dryRun?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/archive-client",
3
- "version": "0.21.0",
3
+ "version": "0.23.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",