@jbrowse/cli 4.3.0 → 5.0.0-beta.2
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 +413 -57
- package/bin/run +1 -1
- package/bundle/index.js +550 -2659
- package/dist/base.js +0 -3
- package/dist/bin.js +2 -1
- package/dist/cliFetch.js +3 -0
- package/dist/commands/add-assembly/index.js +46 -19
- package/dist/commands/add-assembly/utils.js +204 -290
- package/dist/commands/add-connection.js +73 -79
- package/dist/commands/add-track-json.js +24 -12
- package/dist/commands/add-track-utils/adapter-utils.js +222 -288
- package/dist/commands/add-track-utils/file-operations.js +27 -11
- package/dist/commands/add-track-utils/multiwig.js +47 -0
- package/dist/commands/add-track-utils/track-config.js +68 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +201 -62
- package/dist/commands/admin-server/index.js +30 -49
- package/dist/commands/admin-server/utils.js +235 -125
- package/dist/commands/create.js +18 -34
- package/dist/commands/make-density/density-generator.js +180 -0
- package/dist/commands/make-density/index.js +139 -0
- package/dist/commands/make-pif/index.js +128 -11
- package/dist/commands/make-pif/pif-generator.js +249 -43
- package/dist/commands/process-utils.js +13 -2
- package/dist/commands/remove-track.js +20 -13
- package/dist/commands/set-default-session.js +35 -31
- package/dist/commands/shared/config-operations.js +3 -10
- package/dist/commands/shared/density.js +12 -0
- package/dist/commands/shared/sort-utils.js +67 -10
- package/dist/commands/shared/validators.js +38 -3
- package/dist/commands/sort-bed.js +2 -33
- package/dist/commands/sort-gff.js +2 -33
- package/dist/commands/text-index/adapter-utils.js +6 -23
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +35 -7
- package/dist/commands/text-index/config-utils.js +60 -83
- package/dist/commands/text-index/file-list.js +32 -18
- package/dist/commands/text-index/index.js +4 -4
- package/dist/commands/text-index/indexing-utils.js +49 -73
- package/dist/commands/text-index/per-track.js +44 -31
- package/dist/commands/text-index/validators.js +1 -1
- package/dist/commands/upgrade.js +20 -32
- package/dist/commands/validate/configManifest.generated.js +5765 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +97 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +522 -0
- package/dist/index.js +104 -50
- package/dist/types/common.js +3 -27
- package/dist/utils.js +309 -128
- package/dist/version.js +1 -1
- package/package.json +8 -10
- package/dist/commands/make-pif/cigar-utils.js +0 -24
- package/dist/commands/make-pif/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -1,55 +1,261 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Transform } from 'node:stream';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { createGunzip } from 'node:zlib';
|
|
7
|
+
import { coarsenCigar, csToCigar, flipCigar, flipCoarseCigar, swapCoarseCigar, swapIndelCigar, } from '@jbrowse/cigar-utils';
|
|
8
|
+
// Default gap (bp) of the coarse tier's coarse CIGAR: how far a straight line
|
|
9
|
+
// across one of its runs may be from the alignment's real path. Indels longer
|
|
10
|
+
// than half of it keep their letter; shorter ones fold into the runs. 10kb
|
|
11
|
+
// matches the adapter's default coarseBpPerPxThreshold, so at the zoom where the
|
|
12
|
+
// tier is served the bound is ~1px and a folded indel was sub-pixel anyway.
|
|
13
|
+
export const DEFAULT_COARSE_GAP = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* The one meta line a PIF carries, sorted first by the C-locale sort and kept
|
|
16
|
+
* by tabix as a header. It states the format generation, the tiers written,
|
|
17
|
+
* the coarse tier's accuracy bound (`--coarse`), and whether every input row
|
|
18
|
+
* had a CIGAR — the facts a reader cannot recover from the rows.
|
|
19
|
+
*/
|
|
20
|
+
export function pifHeader(coarseGap, stats) {
|
|
21
|
+
const cigars = stats.cigarRows === 0
|
|
22
|
+
? 'none'
|
|
23
|
+
: stats.cigarRows === stats.rows && stats.unboundedRows === 0
|
|
24
|
+
? 'all'
|
|
25
|
+
: 'some';
|
|
26
|
+
const tiers = coarseGap === undefined ? 'fine' : 'fine,coarse';
|
|
27
|
+
const bound = coarseGap === undefined ? '' : `\tcoarse:i:${coarseGap}`;
|
|
28
|
+
return `#pif\tversion:i:1\ttiers:Z:${tiers}${bound}\tcigars:Z:${cigars}\n`;
|
|
29
|
+
}
|
|
30
|
+
function panSNSample(refName) {
|
|
31
|
+
const i = refName.indexOf('#');
|
|
32
|
+
return i === -1 ? undefined : refName.slice(0, i);
|
|
33
|
+
}
|
|
34
|
+
function addPanSNPair(stats, c1, c2) {
|
|
35
|
+
const a = panSNSample(c1);
|
|
36
|
+
const b = panSNSample(c2);
|
|
37
|
+
if (a !== undefined) {
|
|
38
|
+
stats.samples.add(a);
|
|
39
|
+
}
|
|
40
|
+
if (b !== undefined) {
|
|
41
|
+
stats.samples.add(b);
|
|
42
|
+
}
|
|
43
|
+
if (a !== undefined && b !== undefined && a !== b) {
|
|
44
|
+
stats.pairs.add(a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The sample pairs a complete all-vs-all would state but this file does not.
|
|
49
|
+
* Empty for a pairwise PAF (no PanSN names) and for a genuinely complete file.
|
|
50
|
+
*/
|
|
51
|
+
export function missingPairs({ samples, pairs }) {
|
|
52
|
+
const sorted = [...samples].sort();
|
|
53
|
+
const out = [];
|
|
54
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
55
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
56
|
+
const a = sorted[i];
|
|
57
|
+
const b = sorted[j];
|
|
58
|
+
if (!pairs.has(`${a}\u0000${b}`)) {
|
|
59
|
+
out.push([a, b]);
|
|
20
60
|
}
|
|
21
|
-
// Write the second line and handle backpressure
|
|
22
|
-
await writeWithBackpressure(`${[`q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...rest].join('\t')}\n`);
|
|
23
61
|
}
|
|
24
62
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
// The coarse row's `cr:Z:` value, or nothing, and whether withholding it left
|
|
66
|
+
// the row making a claim it cannot keep.
|
|
67
|
+
//
|
|
68
|
+
// `ops` is nothing when the row has no CIGAR, when the fold is a single run
|
|
69
|
+
// (the coordinate columns already say it all), and when the walk does not close
|
|
70
|
+
// on the row's own far corner — clipping ops, a hand-made cg, a cs whose spans
|
|
71
|
+
// don't add up — since the columns are what the fine tier draws and the coarse
|
|
72
|
+
// row must not disagree with them. A fold of several runs with no kept indel is
|
|
73
|
+
// still written: the runs are where a lopsided cluster of small indels bends
|
|
74
|
+
// the path, which a straight ribbon across the row would miss by up to the
|
|
75
|
+
// whole cluster.
|
|
76
|
+
//
|
|
77
|
+
// `unbounded` separates the last of those from the rest. The first two leave a
|
|
78
|
+
// tagless row a reader may take as one run within `--coarse`, and it is one; a
|
|
79
|
+
// walk that misses its own corner leaves a row that reads the same way and is
|
|
80
|
+
// not, so the header has to stop saying `cigars:Z:all` on its account.
|
|
81
|
+
function coarseFold({ cigar, coarseGap, ownLen, mateLen, }) {
|
|
82
|
+
const coarse = cigar === undefined ? undefined : coarsenCigar(cigar, coarseGap);
|
|
83
|
+
const closed = coarse !== undefined &&
|
|
84
|
+
coarse.ownLen === ownLen &&
|
|
85
|
+
coarse.mateLen === mateLen;
|
|
86
|
+
return {
|
|
87
|
+
ops: closed && (coarse.gapCount > 0 || coarse.opCount > 1)
|
|
88
|
+
? coarse.ops
|
|
89
|
+
: undefined,
|
|
90
|
+
unbounded: cigar !== undefined && !closed,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// A PIF row carries exactly ONE alignment string — `cg:Z:`, in the orientation
|
|
94
|
+
// of the perspective it is indexed under. A minimap2 `cs:Z:` is folded into that
|
|
95
|
+
// CIGAR and never emitted, for two reasons:
|
|
96
|
+
//
|
|
97
|
+
// - `cs` has its own orientation, and reorienting it means reversing op order
|
|
98
|
+
// AND reverse-complementing the spelled-out bases. Keeping an unflipped `cs`
|
|
99
|
+
// beside a flipped `cg` is silently wrong rather than merely lossy:
|
|
100
|
+
// `SyntenyFeature.forEachMismatch` prefers `cs`, so a row from
|
|
101
|
+
// `minimap2 -c --cs` (which emits both tags) drew every indel with reversed
|
|
102
|
+
// sense from the query perspective.
|
|
103
|
+
// - `csToCigar` yields `=`/`X`, so folding a `cs` in is strictly MORE
|
|
104
|
+
// informative than minimap2's own M-style `cg` — which is why `cs` wins when
|
|
105
|
+
// a row carries both. The substituted base letters are the only thing lost;
|
|
106
|
+
// mismatch positions survive as `X`.
|
|
107
|
+
//
|
|
108
|
+
// Returns the rewritten optional-tag list and where the surviving alignment
|
|
109
|
+
// string sits in it (-1 when the row carries none).
|
|
110
|
+
function foldCsIntoCg(tags) {
|
|
111
|
+
const csIdx = tags.findIndex(f => f.startsWith('cs:Z'));
|
|
112
|
+
if (csIdx === -1) {
|
|
113
|
+
return { tags, cigarIdx: tags.findIndex(f => f.startsWith('cg:Z')) };
|
|
114
|
+
}
|
|
115
|
+
const folded = `cg:Z:${csToCigar(tags[csIdx].slice(5))}`;
|
|
116
|
+
const rewritten = tags.flatMap((f, i) => i === csIdx ? [folded] : f.startsWith('cg:Z') ? [] : [f]);
|
|
117
|
+
return { tags: rewritten, cigarIdx: rewritten.indexOf(folded) };
|
|
118
|
+
}
|
|
119
|
+
function pifRow(fields) {
|
|
120
|
+
return `${fields.join('\t')}\n`;
|
|
121
|
+
}
|
|
122
|
+
function processLine(rawLine, coarseGap, stats) {
|
|
123
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
124
|
+
if (line.startsWith('#')) {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
const parts = line.split('\t');
|
|
128
|
+
// A valid PAF row has 12 mandatory columns; anything shorter (blank, or
|
|
129
|
+
// truncated) would produce NaN coords, so skip it rather than emit garbage.
|
|
130
|
+
if (parts.length < 12) {
|
|
131
|
+
stats.skipped++;
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
stats.rows++;
|
|
135
|
+
// rest[0]=num_matches, rest[1]=block_len, rest[2]=mapq, rest[3+]=optional tags
|
|
136
|
+
const [c1, l1, s1, e1, strand, c2, l2, s2, e2, ...rest] = parts;
|
|
137
|
+
addPanSNPair(stats, c1, c2);
|
|
138
|
+
// an incoming cr:Z: (a PIF turned back into PAF, or a tool that adopted the
|
|
139
|
+
// tag) is dropped from both tiers: the fine tier never carries one, and the
|
|
140
|
+
// coarse tier writes its own below
|
|
141
|
+
const { tags, cigarIdx } = foldCsIntoCg(rest.filter(f => !f.startsWith('cr:Z:')));
|
|
142
|
+
const cigar = cigarIdx === -1 ? undefined : tags[cigarIdx].slice(5);
|
|
143
|
+
if (cigar !== undefined) {
|
|
144
|
+
stats.cigarRows++;
|
|
145
|
+
}
|
|
146
|
+
// the t-row keeps the CIGAR as PAF spelled it (target perspective); the q-row
|
|
147
|
+
// re-orients it for the query perspective it is indexed under
|
|
148
|
+
const queryTags = cigar === undefined
|
|
149
|
+
? tags
|
|
150
|
+
: tags.map((f, i) => i === cigarIdx
|
|
151
|
+
? `cg:Z:${strand === '-' ? flipCigar(cigar) : swapIndelCigar(cigar)}`
|
|
152
|
+
: f);
|
|
153
|
+
const fineRows = pifRow([`t${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...tags]) +
|
|
154
|
+
pifRow([`q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...queryTags]);
|
|
155
|
+
if (coarseGap === undefined) {
|
|
156
|
+
return fineRows;
|
|
28
157
|
}
|
|
29
|
-
|
|
30
|
-
|
|
158
|
+
// the coarse row is the fine row with its CIGAR replaced by the fold and
|
|
159
|
+
// every other column and tag verbatim, so a click and the identity coloring
|
|
160
|
+
// read the same on both tiers
|
|
161
|
+
const coarse = cigarIdx === -1 ? tags : tags.filter((_, i) => i !== cigarIdx);
|
|
162
|
+
// the T row's own axis is the target, as the PAF CIGAR is written; the Q row
|
|
163
|
+
// re-orients it for the query, the way the fine tier's cg is
|
|
164
|
+
const { ops: cr, unbounded } = coarseFold({
|
|
165
|
+
cigar,
|
|
166
|
+
coarseGap,
|
|
167
|
+
ownLen: +e2 - +s2,
|
|
168
|
+
mateLen: +e1 - +s1,
|
|
169
|
+
});
|
|
170
|
+
if (unbounded) {
|
|
171
|
+
stats.unboundedRows++;
|
|
31
172
|
}
|
|
173
|
+
const tCr = cr === undefined ? [] : [`cr:Z:${cr}`];
|
|
174
|
+
const qCr = cr === undefined
|
|
175
|
+
? []
|
|
176
|
+
: [`cr:Z:${strand === '-' ? flipCoarseCigar(cr) : swapCoarseCigar(cr)}`];
|
|
177
|
+
return (fineRows +
|
|
178
|
+
pifRow([`T${c2}`, l2, s2, e2, strand, c1, l1, s1, e1, ...coarse, ...tCr]) +
|
|
179
|
+
pifRow([`Q${c1}`, l1, s1, e1, strand, c2, l2, s2, e2, ...coarse, ...qCr]));
|
|
180
|
+
}
|
|
181
|
+
function makePifTransform(coarseGap, stats) {
|
|
182
|
+
let tail = '';
|
|
183
|
+
return new Transform({
|
|
184
|
+
transform(chunk, _enc, callback) {
|
|
185
|
+
const data = tail + chunk.toString('utf8');
|
|
186
|
+
const lastNl = data.lastIndexOf('\n');
|
|
187
|
+
if (lastNl === -1) {
|
|
188
|
+
tail = data;
|
|
189
|
+
callback();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
tail = data.slice(lastNl + 1);
|
|
193
|
+
callback(null, data
|
|
194
|
+
.slice(0, lastNl)
|
|
195
|
+
.split('\n')
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
.map(l => processLine(l, coarseGap, stats))
|
|
198
|
+
.join(''));
|
|
199
|
+
},
|
|
200
|
+
flush(callback) {
|
|
201
|
+
// the header goes out last and sorts first: the counts it states are
|
|
202
|
+
// only known once every row has been seen, and `#` precedes every tier
|
|
203
|
+
// letter in the C locale
|
|
204
|
+
callback(null, (tail ? processLine(tail, coarseGap, stats) : '') +
|
|
205
|
+
pifHeader(coarseGap, stats));
|
|
206
|
+
},
|
|
207
|
+
});
|
|
32
208
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
209
|
+
// resolves to what the pass observed: the PanSN samples (empty for a pairwise
|
|
210
|
+
// PAF), so the caller can suggest the right adapter and assembly names, and the
|
|
211
|
+
// row/skip counts so it can warn on a file that was not PAF at all
|
|
212
|
+
export async function createPIF(filename, stream, coarseGap) {
|
|
213
|
+
const stats = {
|
|
214
|
+
samples: new Set(),
|
|
215
|
+
pairs: new Set(),
|
|
216
|
+
rows: 0,
|
|
217
|
+
cigarRows: 0,
|
|
218
|
+
unboundedRows: 0,
|
|
219
|
+
skipped: 0,
|
|
42
220
|
};
|
|
43
|
-
|
|
44
|
-
|
|
221
|
+
const transform = makePifTransform(coarseGap, stats);
|
|
222
|
+
if (filename) {
|
|
223
|
+
const source = createReadStream(filename);
|
|
224
|
+
await (/\.b?gz$/i.test(filename)
|
|
225
|
+
? pipeline(source, createGunzip(), transform, stream)
|
|
226
|
+
: pipeline(source, transform, stream));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
await pipeline(process.stdin, transform, stream);
|
|
230
|
+
}
|
|
231
|
+
return stats;
|
|
232
|
+
}
|
|
233
|
+
// bgzip is the only stage of this pipeline that cannot use more than one core on
|
|
234
|
+
// its own: sort parallelizes itself and tabix is I/O bound. 4 is enough to take
|
|
235
|
+
// compression off the critical path on the files this command is pointed at
|
|
236
|
+
// without claiming a whole machine.
|
|
237
|
+
export const DEFAULT_BGZIP_THREADS = 4;
|
|
238
|
+
export function spawnSortProcess(outputFile, useCsi, threads = DEFAULT_BGZIP_THREADS) {
|
|
239
|
+
// -s: tabix wants the rows ordered by (refName, start) and by nothing else,
|
|
240
|
+
// so the whole-line comparison a non-stable sort falls back to on a tie is
|
|
241
|
+
// comparing multi-kb CIGARs to pick an order no reader can observe
|
|
242
|
+
const sortCmd = `sort -s -t"\`printf '\\t'\`" -k1,1 -k3,3n`;
|
|
243
|
+
const bgzipCommand = `bgzip -@ ${threads} > "$1"`;
|
|
244
|
+
const tabixCommand = `tabix ${useCsi ? '-C ' : ''}-s1 -b3 -e4 -0 "$1"`;
|
|
245
|
+
// `&&` (not `;`) so a bgzip failure aborts before tabix runs and propagates
|
|
246
|
+
// as the pipeline's exit code. The output path is passed as the shell's "$1"
|
|
247
|
+
// positional rather than interpolated into the command string, so a path
|
|
248
|
+
// with shell metacharacters (`"`, `$(...)`, backticks) can't break out and
|
|
249
|
+
// execute — same technique as sort-utils.ts. useCsi is a fixed literal, and
|
|
250
|
+
// the caller has already checked threads is a positive integer.
|
|
251
|
+
const fullCommand = `${sortCmd} | ${bgzipCommand} && ${tabixCommand}`;
|
|
252
|
+
return spawn('sh', ['-c', fullCommand, 'sh', outputFile], {
|
|
253
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
45
254
|
stdio: ['pipe', process.stdout, process.stderr],
|
|
46
255
|
});
|
|
47
256
|
}
|
|
48
257
|
export function getOutputFilename(file, out) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return new Promise(resolve => {
|
|
53
|
-
child.on('close', resolve);
|
|
54
|
-
});
|
|
258
|
+
// strip .paf or .paf.gz so a gzipped input doesn't yield input.paf.gz.pif.gz
|
|
259
|
+
const base = path.basename(file || 'output').replace(/\.paf(\.gz)?$/i, '');
|
|
260
|
+
return out || `${base}.pif.gz`;
|
|
55
261
|
}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export async function waitForProcessClose(child) {
|
|
2
2
|
return new Promise((resolve, reject) => {
|
|
3
|
-
child.on('close', code => {
|
|
4
|
-
resolve(code);
|
|
3
|
+
child.on('close', (code, signal) => {
|
|
4
|
+
resolve({ code, signal });
|
|
5
5
|
});
|
|
6
6
|
child.on('error', err => {
|
|
7
7
|
reject(err);
|
|
8
8
|
});
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
|
+
// A downstream consumer closing the pipe early — `jbrowse sort-bed big.bed |
|
|
12
|
+
// head` — is that consumer's choice, not a failure of ours. `sh` reports a
|
|
13
|
+
// pipeline member's death by SIGPIPE as 128+13 rather than propagating the
|
|
14
|
+
// signal, so both spellings have to be recognized.
|
|
15
|
+
export function diedOfSigpipe({ code, signal }) {
|
|
16
|
+
return signal === 'SIGPIPE' || code === 128 + 13;
|
|
17
|
+
}
|
|
18
|
+
// for an error message: the exit code, or the signal when the child was killed
|
|
19
|
+
export function describeExit({ code, signal }) {
|
|
20
|
+
return code === null ? `signal ${signal}` : `code ${code}`;
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseArgs } from 'util';
|
|
2
|
-
import { printHelp,
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { printHelp, readConfigFile, requirePositional, resolveConfigPath, writeJsonFile, } from '../utils.js';
|
|
3
3
|
export async function run(args) {
|
|
4
4
|
const options = {
|
|
5
5
|
help: {
|
|
@@ -21,30 +21,37 @@ export async function run(args) {
|
|
|
21
21
|
allowPositionals: true,
|
|
22
22
|
});
|
|
23
23
|
const description = 'Remove a track configuration from a JBrowse 2 configuration. Be aware that this can cause crashes in saved sessions that refer to this track!';
|
|
24
|
-
const
|
|
24
|
+
const usage = 'jbrowse remove-track <trackId> [options]';
|
|
25
|
+
const examples = [
|
|
26
|
+
'# remove a track from the config.json in the current directory',
|
|
27
|
+
'$ jbrowse remove-track my_track_id',
|
|
28
|
+
'',
|
|
29
|
+
'# remove a track from a config.json in a specific installation directory',
|
|
30
|
+
'$ jbrowse remove-track my_track_id --out /path/to/jb2/',
|
|
31
|
+
'',
|
|
32
|
+
'# remove a track from a specific config file',
|
|
33
|
+
'$ jbrowse remove-track my_track_id --target /path/to/jb2/config.json',
|
|
34
|
+
];
|
|
25
35
|
if (flags.help) {
|
|
26
36
|
printHelp({
|
|
27
37
|
description,
|
|
28
38
|
examples,
|
|
29
|
-
usage
|
|
39
|
+
usage,
|
|
30
40
|
options,
|
|
31
41
|
});
|
|
32
42
|
return;
|
|
33
43
|
}
|
|
34
44
|
const trackId = positionals[0];
|
|
35
|
-
|
|
36
|
-
throw new Error('Missing required argument: trackId\nUsage: jbrowse remove-track <trackId> [options]');
|
|
37
|
-
}
|
|
45
|
+
requirePositional(trackId, 'trackId', usage);
|
|
38
46
|
const target = await resolveConfigPath(flags.target, flags.out);
|
|
39
|
-
const config = await
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (originalLength === newLength) {
|
|
47
|
+
const config = await readConfigFile(target);
|
|
48
|
+
const tracks = config.tracks ?? [];
|
|
49
|
+
const remaining = tracks.filter(track => track.trackId !== trackId);
|
|
50
|
+
if (remaining.length === tracks.length) {
|
|
44
51
|
console.log(`No track found with trackId: ${trackId}`);
|
|
45
52
|
}
|
|
46
53
|
else {
|
|
47
|
-
await writeJsonFile(target, config);
|
|
54
|
+
await writeJsonFile(target, { ...config, tracks: remaining });
|
|
48
55
|
console.log(`Removed track with trackId: ${trackId} from ${target}`);
|
|
49
56
|
}
|
|
50
57
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import parseJSON from 'json-parse-better-errors';
|
|
4
|
-
import { printHelp, readJsonFile, resolveConfigPath, writeJsonFile, } from "../utils.js";
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { isRecord, printHelp, readConfigFile, readJsonFile, resolveConfigPath, writeJsonFile, } from '../utils.js';
|
|
5
3
|
const description = 'Set a default session with views and tracks';
|
|
6
4
|
const examples = [
|
|
7
5
|
'# set default session for the config.json in your current directory',
|
|
@@ -10,20 +8,28 @@ const examples = [
|
|
|
10
8
|
'# make session.json the defaultSession on the specified target config.json file',
|
|
11
9
|
'$ jbrowse set-default-session --target /path/to/jb2/installation/config.json --session session.json',
|
|
12
10
|
'',
|
|
13
|
-
'#
|
|
14
|
-
'$ jbrowse set-default-session --
|
|
11
|
+
'# override the name stored in the session file',
|
|
12
|
+
'$ jbrowse set-default-session --session session.json --name "My default view"',
|
|
13
|
+
'',
|
|
14
|
+
'# pipe a session in from another program',
|
|
15
|
+
`$ jq '.defaultSession' other/config.json | jbrowse set-default-session --session -`,
|
|
16
|
+
'',
|
|
17
|
+
'# print the current default session',
|
|
18
|
+
'$ jbrowse set-default-session --currentSession',
|
|
19
|
+
'',
|
|
20
|
+
'# remove the existing default session',
|
|
21
|
+
'$ jbrowse set-default-session --delete',
|
|
15
22
|
];
|
|
16
23
|
const options = {
|
|
17
24
|
session: {
|
|
18
25
|
type: 'string',
|
|
19
26
|
short: 's',
|
|
20
|
-
description: 'set path to a file containing session in json format (required, unless using delete/currentSession flags)',
|
|
27
|
+
description: 'set path to a file containing session in json format, or "-" to read it from stdin (required, unless using delete/currentSession flags)',
|
|
21
28
|
},
|
|
22
29
|
name: {
|
|
23
30
|
type: 'string',
|
|
24
31
|
short: 'n',
|
|
25
|
-
description: 'Give a name for the default session',
|
|
26
|
-
default: 'New Default Session',
|
|
32
|
+
description: 'Give a name for the default session (overrides any name in the session file; defaults to "New Default Session")',
|
|
27
33
|
},
|
|
28
34
|
currentSession: {
|
|
29
35
|
type: 'boolean',
|
|
@@ -47,14 +53,14 @@ export async function run(args) {
|
|
|
47
53
|
printHelp({
|
|
48
54
|
description,
|
|
49
55
|
examples,
|
|
50
|
-
usage: 'jbrowse
|
|
56
|
+
usage: 'jbrowse set-default-session [options]',
|
|
51
57
|
options,
|
|
52
58
|
});
|
|
53
59
|
return;
|
|
54
60
|
}
|
|
55
|
-
const { session, currentSession, delete: deleteDefaultSession } = runFlags;
|
|
61
|
+
const { session, name, currentSession, delete: deleteDefaultSession, } = runFlags;
|
|
56
62
|
const target = await resolveConfigPath(runFlags.target, runFlags.out);
|
|
57
|
-
const configContents = await
|
|
63
|
+
const configContents = await readConfigFile(target);
|
|
58
64
|
if (deleteDefaultSession) {
|
|
59
65
|
configContents.defaultSession = undefined;
|
|
60
66
|
await writeJsonFile(target, configContents);
|
|
@@ -65,30 +71,28 @@ export async function run(args) {
|
|
|
65
71
|
else if (!session) {
|
|
66
72
|
throw new Error('Please provide a --session file');
|
|
67
73
|
}
|
|
68
|
-
else
|
|
74
|
+
else {
|
|
75
|
+
const fileSession = await readDefaultSessionFile(session);
|
|
69
76
|
await writeJsonFile(target, {
|
|
70
77
|
...configContents,
|
|
71
|
-
|
|
78
|
+
// precedence: explicit --name > the session file's own name > fallback
|
|
79
|
+
defaultSession: {
|
|
80
|
+
name: 'New Default Session',
|
|
81
|
+
...fileSession,
|
|
82
|
+
...(name !== undefined ? { name } : {}),
|
|
83
|
+
},
|
|
72
84
|
});
|
|
73
85
|
}
|
|
74
86
|
}
|
|
75
87
|
async function readDefaultSessionFile(defaultSessionFile) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new Error(
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
const session = parseJSON(defaultSessionJson);
|
|
87
|
-
// return top-level "session" if it exists, such as in files created by
|
|
88
|
-
// "File -> Export session"
|
|
89
|
-
return session.session || session;
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
throw new Error('Could not parse the given default session file');
|
|
88
|
+
const contents = await readJsonFile(defaultSessionFile);
|
|
89
|
+
// unwrap the top-level "session" if it exists, such as in files created by
|
|
90
|
+
// "File -> Export session"
|
|
91
|
+
const session = contents.session ?? contents;
|
|
92
|
+
// spreading a string or an array here silently wrote a defaultSession of
|
|
93
|
+
// numeric keys, so say what is wrong with the file instead
|
|
94
|
+
if (!isRecord(session)) {
|
|
95
|
+
throw new Error(`${defaultSessionFile} does not contain a session object${contents.session === undefined ? '' : ' under its "session" key'}`);
|
|
93
96
|
}
|
|
97
|
+
return session;
|
|
94
98
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { debug, writeJsonFile } from
|
|
2
|
-
|
|
3
|
-
* Generic function to find and update or add an item to a config array
|
|
4
|
-
* Returns the updated config and whether an item was overwritten
|
|
5
|
-
*/
|
|
6
|
-
export function findAndUpdateOrAdd({ items, newItem, idField, getId, allowOverwrite, itemType, }) {
|
|
1
|
+
import { debug, writeJsonFile } from '../../utils.js';
|
|
2
|
+
export function findAndUpdateOrAdd({ items, newItem, idField, getId, force, itemType, }) {
|
|
7
3
|
const newId = getId(newItem);
|
|
8
4
|
const idx = items.findIndex(item => getId(item) === newId);
|
|
9
5
|
if (idx !== -1) {
|
|
10
6
|
debug(`Found existing ${itemType} ${newId} in configuration`);
|
|
11
|
-
if (
|
|
7
|
+
if (force) {
|
|
12
8
|
debug(`Overwriting ${itemType} ${newId} in configuration`);
|
|
13
9
|
const updatedItems = [...items];
|
|
14
10
|
updatedItems[idx] = newItem;
|
|
@@ -22,9 +18,6 @@ export function findAndUpdateOrAdd({ items, newItem, idField, getId, allowOverwr
|
|
|
22
18
|
return { updatedItems: [...items, newItem], wasOverwritten: false };
|
|
23
19
|
}
|
|
24
20
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Saves a config file and reports the result to the user
|
|
27
|
-
*/
|
|
28
21
|
export async function saveConfigAndReport({ config, target, itemType, itemName, itemId, wasOverwritten, }) {
|
|
29
22
|
debug(`Writing configuration to file ${target}`);
|
|
30
23
|
await writeJsonFile(target, config);
|
|
@@ -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,7 +1,11 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { fileSync } from 'tmp';
|
|
4
|
+
import { printHelp } from '../../utils.js';
|
|
5
|
+
import { describeExit, diedOfSigpipe, waitForProcessClose, } from '../process-utils.js';
|
|
6
|
+
import { validateFileArgument, validateRequiredCommands } from './validators.js';
|
|
3
7
|
export const BED_CONFIG = {
|
|
4
|
-
description: '
|
|
8
|
+
description: 'Sort a BED file for tabix. It is `sort -k1,1 -k2,2n` with LC_ALL=C and a tab separator, plus every line starting with # kept at the top rather than sorted into the data. Takes a file, or the same data on stdin',
|
|
5
9
|
examples: [
|
|
6
10
|
'# sort bed and pipe to bgzip',
|
|
7
11
|
'$ jbrowse sort-bed input.bed | bgzip > sorted.bed.gz',
|
|
@@ -13,7 +17,7 @@ export const BED_CONFIG = {
|
|
|
13
17
|
fileType: 'bed',
|
|
14
18
|
};
|
|
15
19
|
export const GFF_CONFIG = {
|
|
16
|
-
description: '
|
|
20
|
+
description: 'Sort a GFF or GTF for tabix — the two share a refname/start column layout. It is `sort -k1,1 -k4,4n` with LC_ALL=C and a tab separator, plus every line starting with # kept at the top rather than sorted into the data. Takes a file, or the same data on stdin',
|
|
17
21
|
examples: [
|
|
18
22
|
'# sort gff and pipe to bgzip',
|
|
19
23
|
'$ jbrowse sort-gff input.gff | bgzip > sorted.gff.gz',
|
|
@@ -21,6 +25,10 @@ export const GFF_CONFIG = {
|
|
|
21
25
|
'',
|
|
22
26
|
'# sort gff from stdin',
|
|
23
27
|
'$ cat input.gff | jbrowse sort-gff | bgzip > sorted.gff.gz',
|
|
28
|
+
'',
|
|
29
|
+
'# also works on GTF',
|
|
30
|
+
'$ jbrowse sort-gff input.gtf | bgzip > sorted.gtf.gz',
|
|
31
|
+
'$ tabix -p gff sorted.gtf.gz',
|
|
24
32
|
],
|
|
25
33
|
sortColumn: 4,
|
|
26
34
|
fileType: 'gff',
|
|
@@ -31,20 +39,69 @@ function getMinimalEnvironment() {
|
|
|
31
39
|
LC_ALL: 'C',
|
|
32
40
|
};
|
|
33
41
|
}
|
|
42
|
+
// The file path is passed to the shell as the positional argument "$1" rather
|
|
43
|
+
// than interpolated into the command string, so a path containing shell
|
|
44
|
+
// metacharacters (`"`, `$(...)`, backticks) cannot break out and execute. Only
|
|
45
|
+
// sortColumn, an integer from the BED/GFF configs, is interpolated.
|
|
46
|
+
function sortPipeline(sortColumn) {
|
|
47
|
+
return `grep "^#" "$1"; grep -v "^#" "$1" | sort -t"\`printf '\\t'\`" -k1,1 -k${sortColumn},${sortColumn}n`;
|
|
48
|
+
}
|
|
34
49
|
function createSortCommandForStdin(sortColumn) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
// discardDescriptor: we only want the path — the shell below opens the file
|
|
51
|
+
// itself, so tmp's own handle would just be a descriptor left open for the
|
|
52
|
+
// life of the process
|
|
53
|
+
const tmpFile = fileSync({
|
|
54
|
+
prefix: 'jbrowse-sort',
|
|
55
|
+
discardDescriptor: true,
|
|
56
|
+
}).name;
|
|
57
|
+
// trap on EXIT removes the temp file whether or not the sort pipeline
|
|
58
|
+
// succeeds (a bare `&& rm` leaks the file when the pipeline fails)
|
|
59
|
+
return {
|
|
60
|
+
command: `trap 'rm -f "$1"' EXIT; cat > "$1" && (${sortPipeline(sortColumn)})`,
|
|
61
|
+
pathArg: tmpFile,
|
|
62
|
+
};
|
|
38
63
|
}
|
|
39
64
|
function createSortCommandForFile(file, sortColumn) {
|
|
40
|
-
return `(
|
|
65
|
+
return { command: `(${sortPipeline(sortColumn)})`, pathArg: file };
|
|
41
66
|
}
|
|
42
67
|
export function spawnSortProcess(file, sortColumn) {
|
|
43
|
-
const command = file
|
|
68
|
+
const { command, pathArg } = file
|
|
44
69
|
? createSortCommandForFile(file, sortColumn)
|
|
45
70
|
: createSortCommandForStdin(sortColumn);
|
|
46
|
-
|
|
71
|
+
// 'sh' becomes $0, pathArg becomes $1 inside the command
|
|
72
|
+
return spawn('sh', ['-c', command, 'sh', pathArg], {
|
|
47
73
|
env: getMinimalEnvironment(),
|
|
48
74
|
stdio: 'inherit',
|
|
49
75
|
});
|
|
50
76
|
}
|
|
77
|
+
export async function runSort(config, commandName, args) {
|
|
78
|
+
const options = { help: { type: 'boolean', short: 'h' } };
|
|
79
|
+
const { values: flags, positionals } = parseArgs({
|
|
80
|
+
args,
|
|
81
|
+
options,
|
|
82
|
+
allowPositionals: true,
|
|
83
|
+
});
|
|
84
|
+
if (flags.help) {
|
|
85
|
+
printHelp({
|
|
86
|
+
description: config.description,
|
|
87
|
+
examples: config.examples,
|
|
88
|
+
usage: `jbrowse ${commandName} [file] [options]`,
|
|
89
|
+
options,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const file = positionals[0];
|
|
94
|
+
validateFileArgument(file, commandName, config.fileType);
|
|
95
|
+
validateRequiredCommands(['sh', 'sort', 'grep']);
|
|
96
|
+
const child = spawnSortProcess(file, config.sortColumn);
|
|
97
|
+
const exit = await waitForProcessClose(child);
|
|
98
|
+
// These commands exist to be piped (`jbrowse sort-bed in.bed | bgzip > …`), so
|
|
99
|
+
// a consumer that stops reading early — `| head`, `| grep -q` — must not be
|
|
100
|
+
// reported as a sort failure. It used to exit 1 with "exited with code 141".
|
|
101
|
+
if (diedOfSigpipe(exit)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (exit.code !== 0) {
|
|
105
|
+
throw new Error(`Sort process exited with ${describeExit(exit)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|