@jbrowse/cli 5.0.0-beta.1 → 5.0.0-beta.10

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.
Files changed (39) hide show
  1. package/README.md +149 -44
  2. package/dist/commands/add-track-utils/adapter-utils.js +47 -31
  3. package/dist/commands/add-track-utils/track-config.js +5 -7
  4. package/dist/commands/add-track.js +31 -9
  5. package/dist/commands/admin-server/index.js +13 -5
  6. package/dist/commands/admin-server/utils.js +8 -6
  7. package/dist/commands/make-density/density-generator.js +180 -0
  8. package/dist/commands/make-density/index.js +139 -0
  9. package/dist/commands/make-pif/index.js +62 -27
  10. package/dist/commands/make-pif/pif-generator.js +98 -132
  11. package/dist/commands/shared/density.js +12 -0
  12. package/dist/commands/text-index/adapter-utils.js +17 -7
  13. package/dist/commands/text-index/aggregate.js +5 -5
  14. package/dist/commands/text-index/command.js +3 -1
  15. package/dist/commands/text-index/config-utils.js +7 -7
  16. package/dist/commands/text-index/indexing-utils.js +21 -49
  17. package/dist/commands/validate/configManifest.generated.js +4064 -1845
  18. package/dist/commands/validate/configSchema.generated.js +20400 -0
  19. package/dist/commands/validate/index.js +21 -5
  20. package/dist/commands/validate/liftConfig.js +34 -0
  21. package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
  22. package/dist/commands/validate/markRules/autoBin.js +27 -0
  23. package/dist/commands/validate/markRules/colorScale.js +244 -0
  24. package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
  25. package/dist/commands/validate/markRules/jexlStrings.js +23 -0
  26. package/dist/commands/validate/markRules/markProblems.js +532 -0
  27. package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
  28. package/dist/commands/validate/markRules/markSpecs.js +96 -0
  29. package/dist/commands/validate/markRules/markVocabulary.js +57 -0
  30. package/dist/commands/validate/markRules/shapeNameList.js +7 -0
  31. package/dist/commands/validate/markRules/stepChannels.js +55 -0
  32. package/dist/commands/validate/schemaValidate.js +425 -0
  33. package/dist/commands/validate/validateConfig.js +304 -259
  34. package/dist/index.js +5 -0
  35. package/dist/version.js +1 -1
  36. package/package.json +6 -10
  37. package/bin/run +0 -4
  38. package/bundle/index.js +0 -1569
  39. package/dist/commands/make-pif/cigar-utils.js +0 -67
@@ -4,14 +4,33 @@ import path from 'node:path';
4
4
  import { Transform } from 'node:stream';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  import { createGunzip } from 'node:zlib';
7
- import { csToCigar, flipCigar, pafIdentity, swapIndelCigar, } from '@jbrowse/cigar-utils';
8
- import { splitCigarOnLargeGaps } from './cigar-utils.js';
9
- // Default split gap (bp) for the coarse tier. A row is broken into multiple
10
- // coarse pieces wherever a CIGAR insertion/deletion is at least this long, so
11
- // each coarse row's bounding box stays tight. 10kb matches the adapter's
12
- // default coarseBpPerPxThreshold (~1px at the zoom where the coarse tier is
13
- // served), so smaller gaps that would be sub-pixel there don't fragment rows.
14
- export const DEFAULT_COARSE_SPLIT_GAP = 10_000;
7
+ import { coarsenCigar, csToCigar, flipCigar, flipCoarseCigar, swapCoarseCigar, swapIndelCigar, } from '@jbrowse/cigar-utils';
8
+ import { version } from '../../version.js';
9
+ // Default gap (bp) of the coarse tier's coarse CIGAR: how far a straight line
10
+ // across one of its runs may be from the alignment's real path. Indels longer
11
+ // than half of it keep their letter; shorter ones fold into the runs. 10kb
12
+ // matches the adapter's default coarseBpPerPxThreshold, so at the zoom where the
13
+ // tier is served the bound is ~1px and a folded indel was sub-pixel anyway.
14
+ export const DEFAULT_COARSE_GAP = 10_000;
15
+ // Generation 2 (2026-09): every row carries `pi:i:`, the input row's index,
16
+ // and the header names the writer.
17
+ export const PIF_FORMAT_VERSION = 2;
18
+ /**
19
+ * The one meta line a PIF carries, sorted first by the C-locale sort and kept
20
+ * by tabix as a header. It states the format generation, the writer, the
21
+ * tiers written, the coarse tier's accuracy bound (`--coarse`), and whether
22
+ * every input row had a CIGAR — the facts a reader cannot recover from the rows.
23
+ */
24
+ export function pifHeader(coarseGap, stats) {
25
+ const cigars = stats.cigarRows === 0
26
+ ? 'none'
27
+ : stats.cigarRows === stats.rows
28
+ ? 'all'
29
+ : 'some';
30
+ const tiers = coarseGap === undefined ? 'fine' : 'fine,coarse';
31
+ const bound = coarseGap === undefined ? '' : `\tcoarse:i:${coarseGap}`;
32
+ return `#pif\tversion:i:${PIF_FORMAT_VERSION}\twriter:Z:jbrowse-cli/${version}\ttiers:Z:${tiers}${bound}\tcigars:Z:${cigars}\n`;
33
+ }
15
34
  function panSNSample(refName) {
16
35
  const i = refName.indexOf('#');
17
36
  return i === -1 ? undefined : refName.slice(0, i);
@@ -47,34 +66,23 @@ export function missingPairs({ samples, pairs }) {
47
66
  }
48
67
  return out;
49
68
  }
50
- function coarsePieces({ cigar, strand, tstart, tend, qstart, qend, numMatches, blockLen, splitGap, }) {
51
- const whole = { tstart, tend, qstart, qend, numMatches, blockLen };
52
- const split = cigar && splitGap > 0
53
- ? splitCigarOnLargeGaps({
54
- cigar,
55
- strand,
56
- tstart,
57
- tend,
58
- qstart,
59
- qend,
60
- splitGap,
61
- })
69
+ // The coarse row's `cr:Z:` value, or nothing when the row has no CIGAR or the
70
+ // fold is a single run that closes on the coordinate columns, which then say it
71
+ // all. A fold of several runs with no kept indel is still written: a lopsided
72
+ // cluster of small indels bends the path by their sum. So is a fold whose walk
73
+ // misses the row's own far corner (clipping ops, a hand-made cg, a cs whose
74
+ // spans don't add up): the fine tier walks that CIGAR from the start corner
75
+ // without checking, and the coarse tier draws the same path only if it walks
76
+ // the same fold.
77
+ function coarseFold({ cigar, coarseGap, ownLen, mateLen, }) {
78
+ if (cigar === undefined) {
79
+ return undefined;
80
+ }
81
+ const coarse = coarsenCigar(cigar, coarseGap);
82
+ const closed = coarse.ownLen === ownLen && coarse.mateLen === mateLen;
83
+ return !closed || coarse.gapCount > 0 || coarse.opCount > 1
84
+ ? coarse.ops
62
85
  : undefined;
63
- // The walk's reconstruction is taken only when it CLOSED on the row's own far
64
- // corner (see `CigarSplit.closed`) — otherwise the columns are what the fine
65
- // tier draws and the coarse row must not drift off them.
66
- //
67
- // Closure rather than a piece count: `segments.length < 2` read as "nothing to
68
- // split on", but a large gap at either END also yields a single piece, a
69
- // genuine tightening that was being thrown away for the loose box. It is the
70
- // ordinary case below the default `--coarse`. The count also never noticed a
71
- // MULTI-piece walk that failed to close, which is the case it was written for.
72
- return split?.closed && split.segments.length > 0
73
- ? split.segments.map(seg => ({
74
- ...seg,
75
- numMatches: blockLen > 0 ? Math.round((numMatches * seg.blockLen) / blockLen) : 0,
76
- }))
77
- : [whole];
78
86
  }
79
87
  // A PIF row carries exactly ONE alignment string — `cg:Z:`, in the orientation
80
88
  // of the perspective it is indexed under. A minimap2 `cs:Z:` is folded into that
@@ -102,34 +110,11 @@ function foldCsIntoCg(tags) {
102
110
  const rewritten = tags.flatMap((f, i) => i === csIdx ? [folded] : f.startsWith('cg:Z') ? [] : [f]);
103
111
  return { tags: rewritten, cigarIdx: rewritten.indexOf(folded) };
104
112
  }
105
- // Coarse identity must match the fine tier exactly, or coloring jumps at the
106
- // zoom where the view switches tiers. So it is written off the SAME function the
107
- // adapters read the fine tier with (de:f: tag, then odgi's id:f:, then
108
- // num_matches/block_len) rather than a private copy of that chain — a copy is
109
- // what previously skipped the id:f: rung. A CIGAR recompute is deliberately not
110
- // in the chain at all: a cg (M-style) CIGAR folds mismatches into M, so it would
111
- // report ~0 divergence (spurious 100% identity) for a divergent alignment.
112
- //
113
- // When the row's own de:f: is what the reader lands on, that string is passed
114
- // through byte-for-byte rather than recomputed: a round trip through toFixed(6)
115
- // would silently truncate a 7-decimal tag and drift the two tiers apart by the
116
- // exact mechanism this is guarding against.
117
- function coarseDivergence(tags, numMatches, blockLen) {
118
- const rawDe = tags.find(f => f.startsWith('de:f:'))?.slice(5);
119
- const identity = pafIdentity({
120
- de: rawDe,
121
- id: tags.find(f => f.startsWith('id:f:'))?.slice(5),
122
- numMatches,
123
- blockLen,
124
- });
125
- return rawDe !== undefined && 1 - +rawDe === identity
126
- ? rawDe
127
- : (1 - identity).toFixed(6);
128
- }
129
113
  function pifRow(fields) {
130
114
  return `${fields.join('\t')}\n`;
131
115
  }
132
- function processLine(line, coarseSplitGap, stats) {
116
+ function processLine(rawLine, coarseGap, stats) {
117
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
133
118
  if (line.startsWith('#')) {
134
119
  return '';
135
120
  }
@@ -144,8 +129,18 @@ function processLine(line, coarseSplitGap, stats) {
144
129
  // rest[0]=num_matches, rest[1]=block_len, rest[2]=mapq, rest[3+]=optional tags
145
130
  const [c1, l1, s1, e1, strand, c2, l2, s2, e2, ...rest] = parts;
146
131
  addPanSNPair(stats, c1, c2);
147
- const { tags, cigarIdx } = foldCsIntoCg(rest);
132
+ // an incoming cr:Z: or pi:i: (a PIF turned back into PAF, or a tool that
133
+ // adopted the tags) is dropped: the coarse tier writes its own fold below,
134
+ // and every row of this alignment gets this pass's row index as `pi`, the
135
+ // one id its four rows share across perspectives and tiers
136
+ const { tags, cigarIdx } = foldCsIntoCg([
137
+ ...rest.filter(f => !f.startsWith('cr:') && !f.startsWith('pi:')),
138
+ `pi:i:${stats.rows - 1}`,
139
+ ]);
148
140
  const cigar = cigarIdx === -1 ? undefined : tags[cigarIdx].slice(5);
141
+ if (cigar !== undefined) {
142
+ stats.cigarRows++;
143
+ }
149
144
  // the t-row keeps the CIGAR as PAF spelled it (target perspective); the q-row
150
145
  // re-orients it for the query perspective it is indexed under
151
146
  const queryTags = cigar === undefined
@@ -155,73 +150,30 @@ function processLine(line, coarseSplitGap, stats) {
155
150
  : f);
156
151
  const fineRows = pifRow([`t${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...tags]) +
157
152
  pifRow([`q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...queryTags]);
158
- if (coarseSplitGap === undefined) {
153
+ if (coarseGap === undefined) {
159
154
  return fineRows;
160
155
  }
161
- const numMatches = +tags[0];
162
- const blockLen = +tags[1];
163
- const mapq = tags[2];
164
- // Every optional tag except the alignment string itself rides along, so a
165
- // click on a coarse ribbon shows the same attributes as the same alignment
166
- // does zoomed in (rustybam's tags, minimap2's tp/cm/s1, odgi's id). Only the
167
- // CIGAR is dropped — dropping it is the entire point of the tier — and de:f:,
168
- // which is rewritten below. No `cs:Z:` can reach here: it was folded into the
169
- // cg above, since a PIF row carries one alignment string.
170
- const passthrough = tags
171
- .slice(3)
172
- .filter(f => !f.startsWith('cg:Z:') && !f.startsWith('de:f:'));
173
- const de = coarseDivergence(tags, numMatches, blockLen);
156
+ // the coarse row is the fine row with its CIGAR replaced by the fold and
157
+ // every other column and tag verbatim, so a click and the identity coloring
158
+ // read the same on both tiers
159
+ const coarse = cigarIdx === -1 ? tags : tags.filter((_, i) => i !== cigarIdx);
160
+ // the T row's own axis is the target, as the PAF CIGAR is written; the Q row
161
+ // re-orients it for the query, the way the fine tier's cg is
162
+ const cr = coarseFold({
163
+ cigar,
164
+ coarseGap,
165
+ ownLen: +e2 - +s2,
166
+ mateLen: +e1 - +s1,
167
+ });
168
+ const tCr = cr === undefined ? [] : [`cr:Z:${cr}`];
169
+ const qCr = cr === undefined
170
+ ? []
171
+ : [`cr:Z:${strand === '-' ? flipCoarseCigar(cr) : swapCoarseCigar(cr)}`];
174
172
  return (fineRows +
175
- coarsePieces({
176
- cigar,
177
- strand: strand,
178
- tstart: +s2,
179
- tend: +e2,
180
- qstart: +s1,
181
- qend: +e1,
182
- numMatches,
183
- blockLen,
184
- splitGap: coarseSplitGap,
185
- })
186
- .flatMap(piece => {
187
- const tail = [
188
- piece.numMatches,
189
- piece.blockLen,
190
- mapq,
191
- ...passthrough,
192
- `de:f:${de}`,
193
- ];
194
- const { tstart, tend, qstart, qend } = piece;
195
- return [
196
- pifRow([
197
- `T${c2}`,
198
- l2,
199
- tstart,
200
- tend,
201
- strand,
202
- c1,
203
- l1,
204
- qstart,
205
- qend,
206
- ...tail,
207
- ]),
208
- pifRow([
209
- `Q${c1}`,
210
- l1,
211
- qstart,
212
- qend,
213
- strand,
214
- c2,
215
- l2,
216
- tstart,
217
- tend,
218
- ...tail,
219
- ]),
220
- ];
221
- })
222
- .join(''));
173
+ pifRow([`T${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...coarse, ...tCr]) +
174
+ pifRow([`Q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...coarse, ...qCr]));
223
175
  }
224
- function makePifTransform(coarseSplitGap, stats) {
176
+ function makePifTransform(coarseGap, stats) {
225
177
  let tail = '';
226
178
  return new Transform({
227
179
  transform(chunk, _enc, callback) {
@@ -237,25 +189,30 @@ function makePifTransform(coarseSplitGap, stats) {
237
189
  .slice(0, lastNl)
238
190
  .split('\n')
239
191
  .filter(Boolean)
240
- .map(l => processLine(l, coarseSplitGap, stats))
192
+ .map(l => processLine(l, coarseGap, stats))
241
193
  .join(''));
242
194
  },
243
195
  flush(callback) {
244
- callback(null, tail ? processLine(tail, coarseSplitGap, stats) : '');
196
+ // the header goes out last and sorts first: the counts it states are
197
+ // only known once every row has been seen, and `#` precedes every tier
198
+ // letter in the C locale
199
+ callback(null, (tail ? processLine(tail, coarseGap, stats) : '') +
200
+ pifHeader(coarseGap, stats));
245
201
  },
246
202
  });
247
203
  }
248
204
  // resolves to what the pass observed: the PanSN samples (empty for a pairwise
249
205
  // PAF), so the caller can suggest the right adapter and assembly names, and the
250
206
  // row/skip counts so it can warn on a file that was not PAF at all
251
- export async function createPIF(filename, stream, coarseSplitGap) {
207
+ export async function createPIF(filename, stream, coarseGap) {
252
208
  const stats = {
253
209
  samples: new Set(),
254
210
  pairs: new Set(),
255
211
  rows: 0,
212
+ cigarRows: 0,
256
213
  skipped: 0,
257
214
  };
258
- const transform = makePifTransform(coarseSplitGap, stats);
215
+ const transform = makePifTransform(coarseGap, stats);
259
216
  if (filename) {
260
217
  const source = createReadStream(filename);
261
218
  await (/\.b?gz$/i.test(filename)
@@ -267,15 +224,24 @@ export async function createPIF(filename, stream, coarseSplitGap) {
267
224
  }
268
225
  return stats;
269
226
  }
270
- export function spawnSortProcess(outputFile, useCsi) {
271
- const sortCmd = `sort -t"\`printf '\\t'\`" -k1,1 -k3,3n`;
272
- const bgzipCommand = `bgzip > "$1"`;
227
+ // bgzip is the only stage of this pipeline that cannot use more than one core on
228
+ // its own: sort parallelizes itself and tabix is I/O bound. 4 is enough to take
229
+ // compression off the critical path on the files this command is pointed at
230
+ // without claiming a whole machine.
231
+ export const DEFAULT_BGZIP_THREADS = 4;
232
+ export function spawnSortProcess(outputFile, useCsi, threads = DEFAULT_BGZIP_THREADS) {
233
+ // -s: tabix wants the rows ordered by (refName, start) and by nothing else,
234
+ // so the whole-line comparison a non-stable sort falls back to on a tie is
235
+ // comparing multi-kb CIGARs to pick an order no reader can observe
236
+ const sortCmd = `sort -s -t"\`printf '\\t'\`" -k1,1 -k3,3n`;
237
+ const bgzipCommand = `bgzip -@ ${threads} > "$1"`;
273
238
  const tabixCommand = `tabix ${useCsi ? '-C ' : ''}-s1 -b3 -e4 -0 "$1"`;
274
239
  // `&&` (not `;`) so a bgzip failure aborts before tabix runs and propagates
275
240
  // as the pipeline's exit code. The output path is passed as the shell's "$1"
276
241
  // positional rather than interpolated into the command string, so a path
277
242
  // with shell metacharacters (`"`, `$(...)`, backticks) can't break out and
278
- // execute — same technique as sort-utils.ts. useCsi is a fixed literal.
243
+ // execute — same technique as sort-utils.ts. useCsi is a fixed literal, and
244
+ // the caller has already checked threads is a positive integer.
279
245
  const fullCommand = `${sortCmd} | ${bgzipCommand} && ${tabixCommand}`;
280
246
  return spawn('sh', ['-c', fullCommand, 'sh', outputFile], {
281
247
  env: { ...process.env, LC_ALL: 'C' },
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Where the density sidecar for a data file lives: `<file>.density.bw`, with a
3
+ * `.gz`/`.bgz` stripped first so `genes.gff3.gz` yields `genes.gff3.density.bw`
4
+ * rather than a name carrying the compression of a file it is not compressed
5
+ * like.
6
+ *
7
+ * One rule, two commands: `make-density` writes here by default and `add-track`
8
+ * probes here, so a sidecar built with no `--out` is the one the track picks up.
9
+ */
10
+ export function densitySidecarPath(file) {
11
+ return `${file.replace(/\.b?gz$/i, '')}.density.bw`;
12
+ }
@@ -1,9 +1,8 @@
1
- import { sanitizeForFilename, trixFileUris } from '@jbrowse/text-indexing-core';
2
- export function createTrixAdapter(name, assemblyNames, idSuffix = 'index') {
1
+ import { trixFileUris } from '@jbrowse/text-indexing-core';
2
+ export function createTrixAdapter(name, assemblyNames) {
3
3
  const uris = trixFileUris(name);
4
4
  return {
5
5
  type: 'TrixTextSearchAdapter',
6
- textSearchAdapterId: `${sanitizeForFilename(name)}-${idSuffix}`,
7
6
  ixFilePath: {
8
7
  uri: uris.ix,
9
8
  locationType: 'UriLocation',
@@ -12,10 +11,21 @@ export function createTrixAdapter(name, assemblyNames, idSuffix = 'index') {
12
11
  uri: uris.ixx,
13
12
  locationType: 'UriLocation',
14
13
  },
15
- metaFilePath: {
16
- uri: uris.meta,
17
- locationType: 'UriLocation',
18
- },
19
14
  assemblyNames,
20
15
  };
21
16
  }
17
+ // an aggregate entry is the index its files are, so a re-index of the same
18
+ // assembly finds the entry pointing at them however it was written, `.ix`
19
+ // string and `{ uri }` included; an entry with no trix file, a JBrowse 1
20
+ // index, is only ever itself
21
+ export function indexFileOf(entry) {
22
+ if (typeof entry === 'string') {
23
+ return entry;
24
+ }
25
+ const { ixFilePath, uri } = entry;
26
+ return ixFilePath
27
+ ? 'uri' in ixFilePath
28
+ ? ixFilePath.uri
29
+ : ixFilePath.localPath
30
+ : (uri ?? entry);
31
+ }
@@ -1,5 +1,5 @@
1
1
  import { parseCommaSeparatedString, writeJsonFile } from '../../utils.js';
2
- import { createTrixAdapter } from './adapter-utils.js';
2
+ import { createTrixAdapter, indexFileOf } from './adapter-utils.js';
3
3
  import { formatDryRun, getAssemblyNames, getTrackConfigs, loadConfigForIndexing, prepareIndexDriverFlags, } from './config-utils.js';
4
4
  import { indexDriver } from './indexing-utils.js';
5
5
  export async function aggregateIndex(flags) {
@@ -22,7 +22,7 @@ export async function aggregateIndex(flags) {
22
22
  }
23
23
  const trixConf = createTrixAdapter(asm, [asm]);
24
24
  if (!force &&
25
- existing.some(x => x.textSearchAdapterId === trixConf.textSearchAdapterId)) {
25
+ existing.some(x => indexFileOf(x) === indexFileOf(trixConf))) {
26
26
  console.log(`Note: ${asm} has already been indexed with this configuration, use --force to overwrite this assembly. Skipping for now`);
27
27
  continue;
28
28
  }
@@ -44,11 +44,11 @@ export async function aggregateIndex(flags) {
44
44
  if (!dryrun) {
45
45
  await writeJsonFile(configPath, {
46
46
  ...config,
47
- // upsert by adapter id: a Map keyed on it keeps the first occurrence's
47
+ // upsert by index file: a Map keyed on it keeps the first occurrence's
48
48
  // position while the last value wins, so re-indexing an assembly replaces
49
- // its adapter in place and a new one lands at the end
49
+ // its entry in place and a new one lands at the end
50
50
  aggregateTextSearchAdapters: [
51
- ...new Map([...existing, ...written].map(a => [a.textSearchAdapterId, a])).values(),
51
+ ...new Map([...existing, ...written].map(a => [indexFileOf(a), a])).values(),
52
52
  ],
53
53
  });
54
54
  }
@@ -90,7 +90,9 @@ export async function run(args) {
90
90
  `Only tracks with an indexable adapter type (${Object.keys(indexableAdapters).sort().join(', ')}) are indexed; ` +
91
91
  'tracks with other adapter types are skipped automatically.\n\n' +
92
92
  'GTF has no Name/ID attributes, so the default --attributes also match ' +
93
- 'their GTF spellings (gene_name, transcript_name, gene_id, transcript_id).' +
93
+ 'their GTF spellings (gene_name, transcript_name, gene_id, transcript_id). ' +
94
+ 'A GFF3 row with no Name, as in GENCODE, is named by its own gene_name or ' +
95
+ 'transcript_name.' +
94
96
  '\n\n' +
95
97
  '--exclude names types not to index; --include names the only types to ' +
96
98
  'index. Reach for --include when the file draws from a vocabulary you do ' +
@@ -13,13 +13,13 @@ function validatePrefixSize(value) {
13
13
  }
14
14
  export function prepareIndexDriverFlags(flags) {
15
15
  return {
16
- attributes: parseCommaSeparatedString(flags.attributes),
17
- typesToExclude: parseCommaSeparatedString(flags.exclude),
18
- // undefined rather than [] when the flag is absent, because an empty allow
19
- // list has to mean "no allow list" and not "index nothing"
20
- typesToInclude: flags.include
21
- ? parseCommaSeparatedString(flags.include)
22
- : undefined,
16
+ policy: {
17
+ attributes: parseCommaSeparatedString(flags.attributes),
18
+ exclude: parseCommaSeparatedString(flags.exclude),
19
+ include: flags.include
20
+ ? parseCommaSeparatedString(flags.include)
21
+ : undefined,
22
+ },
23
23
  quiet: flags.quiet ?? false,
24
24
  prefixSize: validatePrefixSize(flags.prefixSize),
25
25
  };
@@ -1,44 +1,30 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { Readable } from 'node:stream';
4
- import { TRIX_DIR, generateMeta, guessAdapterFromFileName, indexFiles, trixFilePaths, } from '@jbrowse/text-indexing-core';
1
+ import { guessAdapterFromFileName, writeTrixIndex, } from '@jbrowse/text-indexing-core';
5
2
  import { Presets, SingleBar } from 'cli-progress';
6
- import { ixIxxStream } from 'ixixx';
7
3
  import { supported } from '../../types/common.js';
8
- // every index artifact lands in <outLocation>/trix, so the directory is created
9
- // here rather than by each caller: --dryrun goes nowhere near indexDriver and so
10
- // no longer leaves an empty trix/ behind
11
- function ensureTrixDir(outLocation) {
12
- fs.mkdirSync(path.join(outLocation, TRIX_DIR), { recursive: true });
4
+ function bar(format) {
5
+ return new SingleBar({ format, etaBuffer: 2000 }, Presets.shades_classic);
13
6
  }
14
- async function runIxIxx({ readStream, outLocation, name, prefixSize, quiet, }) {
15
- const progressBar = new SingleBar({ format: '{bar} Sorting and writing index...', etaBuffer: 2000 }, Presets.shades_classic);
16
- if (!quiet) {
17
- progressBar.start(1, 0);
18
- }
19
- const { ix, ixx } = trixFilePaths(outLocation, name);
20
- await ixIxxStream(readStream, ix, ixx, prefixSize);
21
- if (!quiet) {
22
- progressBar.update(1);
23
- progressBar.stop();
24
- }
25
- }
26
- export async function indexDriver({ trackConfigs, attributes, outLocation, name, quiet, typesToExclude, typesToInclude, assemblyNames, prefixSize, }) {
27
- ensureTrixDir(outLocation);
28
- const readStream = Readable.from(indexFiles({
7
+ export async function indexDriver({ trackConfigs, policy, outLocation, name, quiet, assemblyNames, prefixSize, }) {
8
+ let sortBar;
9
+ await writeTrixIndex({
29
10
  tracks: trackConfigs,
30
- attributesToIndex: attributes,
31
11
  outDir: outLocation,
32
- featureTypesToExclude: typesToExclude,
33
- featureTypesToInclude: typesToInclude,
12
+ name,
13
+ policy,
14
+ assemblyNames,
15
+ prefixSize,
16
+ onSort: quiet
17
+ ? undefined
18
+ : () => {
19
+ sortBar = bar('{bar} Sorting and writing index...');
20
+ sortBar.start(1, 0);
21
+ },
34
22
  makeProgress: quiet
35
23
  ? undefined
36
24
  : trackId => {
37
- const progressBar = new SingleBar({
38
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {bar}/{percentage}/{eta} are cli-progress format tokens, not JS interpolation
39
- format: `{bar} ${trackId} {percentage}% | ETA: {eta}s`,
40
- etaBuffer: 2000,
41
- }, Presets.shades_classic);
25
+ const progressBar = bar(
26
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {bar}/{percentage}/{eta} are cli-progress format tokens, not JS interpolation
27
+ `{bar} ${trackId} {percentage}% | ETA: {eta}s`);
42
28
  return {
43
29
  onStart: totalBytes => {
44
30
  progressBar.start(totalBytes, 0);
@@ -51,23 +37,9 @@ export async function indexDriver({ trackConfigs, attributes, outLocation, name,
51
37
  },
52
38
  };
53
39
  },
54
- }));
55
- await runIxIxx({
56
- readStream,
57
- outLocation,
58
- name,
59
- prefixSize,
60
- quiet,
61
- });
62
- generateMeta({
63
- configs: trackConfigs,
64
- attributesToIndex: attributes,
65
- outDir: outLocation,
66
- name,
67
- featureTypesToExclude: typesToExclude,
68
- featureTypesToInclude: typesToInclude,
69
- assemblyNames,
70
40
  });
41
+ sortBar?.update(1);
42
+ sortBar?.stop();
71
43
  }
72
44
  export function prepareFileTrackConfigs(files, fileIds) {
73
45
  return files.map((file, i) => {