@jbrowse/cli 4.3.0 → 5.0.0-beta.1
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 +331 -57
- package/bin/run +1 -1
- package/bundle/index.js +457 -2574
- 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 +173 -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 +178 -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-pif/cigar-utils.js +62 -19
- package/dist/commands/make-pif/index.js +98 -11
- package/dist/commands/make-pif/pif-generator.js +276 -42
- 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/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 +5394 -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 +457 -0
- package/dist/index.js +99 -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/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -1,42 +1,85 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'ChainAdapter',
|
|
9
|
-
'MashMapAdapter',
|
|
10
|
-
'MCScanAnchorsAdapter',
|
|
11
|
-
'MCScanSimpleAnchorsAdapter',
|
|
12
|
-
]);
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isURL } from '../../types/common.js';
|
|
3
|
+
import { parseCommaSeparatedString } from '../../utils.js';
|
|
4
|
+
import { displayDefaultsForTrackType } from '../validate/displayDefaultKeys.js';
|
|
5
|
+
import { suggest } from '../validate/suggest.js';
|
|
6
|
+
import { syntenyAdapterTypes } from './adapter-utils.js';
|
|
7
|
+
import { parseJsonFlag } from './validators.js';
|
|
13
8
|
export function mapLocationForFiles(p, load, subDir) {
|
|
14
9
|
return !p || isURL(p) || load === 'inPlace'
|
|
15
10
|
? p
|
|
16
11
|
: path.join(subDir || '', path.basename(p));
|
|
17
12
|
}
|
|
18
|
-
export function buildTrackConfig({
|
|
19
|
-
|
|
20
|
-
const finalTrackId = trackId || path.basename(location, path.extname(location));
|
|
21
|
-
const finalName = name || finalTrackId;
|
|
22
|
-
const finalAssemblyNames = assemblyNames || configContents.assemblies?.[0]?.name || '';
|
|
23
|
-
const trackConfig = {
|
|
13
|
+
export function buildTrackConfig({ trackType, trackId, name, assemblyNames, category, description, configObj = {}, adapter, }) {
|
|
14
|
+
return {
|
|
24
15
|
type: trackType,
|
|
25
|
-
trackId
|
|
26
|
-
name
|
|
16
|
+
trackId,
|
|
17
|
+
name,
|
|
27
18
|
adapter,
|
|
28
|
-
category: category
|
|
29
|
-
assemblyNames:
|
|
19
|
+
category: category ? parseCommaSeparatedString(category) : undefined,
|
|
20
|
+
assemblyNames: parseCommaSeparatedString(assemblyNames),
|
|
30
21
|
description,
|
|
31
22
|
...configObj,
|
|
32
23
|
};
|
|
33
|
-
|
|
24
|
+
}
|
|
25
|
+
// Fold the --color / --height convenience flags and any --displayDefaults JSON
|
|
26
|
+
// into one displayDefaults object, layered over a displayDefaults already
|
|
27
|
+
// present in --config. Precedence: --config < --displayDefaults < typed flags,
|
|
28
|
+
// so the most specific flag a user reaches for wins. Returns undefined when
|
|
29
|
+
// nothing was supplied, so the track omits displayDefaults entirely.
|
|
30
|
+
//
|
|
31
|
+
// Takes `trackType` so it can refuse a key none of that track's displays
|
|
32
|
+
// declares. `--color` on an alignments track is the reachable case — that
|
|
33
|
+
// display declares `colorBy`, expandTrackConfigShorthand drops the unmatched
|
|
34
|
+
// key with a console warning, and the command otherwise wrote the dead setting
|
|
35
|
+
// and exited 0.
|
|
36
|
+
export function mergeDisplayDefaults({ configObj, color, height, displayDefaults, trackType, }) {
|
|
37
|
+
const fromJson = displayDefaults
|
|
38
|
+
? parseJsonFlag(displayDefaults, '--displayDefaults')
|
|
39
|
+
: {};
|
|
40
|
+
const typed = {};
|
|
41
|
+
if (color !== undefined) {
|
|
42
|
+
typed.color = color;
|
|
43
|
+
}
|
|
44
|
+
if (height !== undefined) {
|
|
45
|
+
const asNumber = Number(height);
|
|
46
|
+
typed.height = Number.isNaN(asNumber) ? height : asNumber;
|
|
47
|
+
}
|
|
48
|
+
const existing = (configObj?.displayDefaults ?? {});
|
|
49
|
+
const merged = { ...existing, ...fromJson, ...typed };
|
|
50
|
+
if (Object.keys(merged).length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
checkDisplayDefaultKeys(merged, trackType);
|
|
54
|
+
return merged;
|
|
55
|
+
}
|
|
56
|
+
// An unknown track type accepts everything: it is a plugin's, and this manifest
|
|
57
|
+
// is only the core plugins'.
|
|
58
|
+
//
|
|
59
|
+
// The message names the display when the track offers one — which most do, and
|
|
60
|
+
// AlignmentsTrack is the case that matters here, since `--color` is the
|
|
61
|
+
// reachable mistake and LinearAlignmentsDisplay is the only place to go look.
|
|
62
|
+
function checkDisplayDefaultKeys(merged, trackType) {
|
|
63
|
+
const { displayTypes, keys } = displayDefaultsForTrackType(trackType);
|
|
64
|
+
if (keys.length === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const unknown = Object.keys(merged).filter(key => !keys.includes(key));
|
|
68
|
+
if (unknown.length > 0) {
|
|
69
|
+
const declarer = displayTypes.length === 1 ? displayTypes[0] : `any ${trackType} display`;
|
|
70
|
+
throw new Error(unknown
|
|
71
|
+
.map(key => {
|
|
72
|
+
const hit = suggest(key, keys);
|
|
73
|
+
return `"${key}" is not a setting ${declarer} declares${hit ? `, did you mean "${hit}"?` : ''}`;
|
|
74
|
+
})
|
|
75
|
+
.join('\n'));
|
|
76
|
+
}
|
|
34
77
|
}
|
|
35
78
|
export function addSyntenyAssemblyNames(adapter, assemblyNames) {
|
|
36
|
-
if (
|
|
79
|
+
if (syntenyAdapterTypes.has(adapter.type)) {
|
|
37
80
|
return {
|
|
38
81
|
...adapter,
|
|
39
|
-
assemblyNames: assemblyNames
|
|
82
|
+
assemblyNames: parseCommaSeparatedString(assemblyNames),
|
|
40
83
|
};
|
|
41
84
|
}
|
|
42
85
|
return adapter;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { isURL } from '../../types/common.js';
|
|
2
|
+
import { parseCommaSeparatedString } from '../../utils.js';
|
|
3
|
+
export function parseJsonFlag(value, flagName) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(value);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error(`${flagName} is not valid JSON: ${value}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function parseConfigFlag(config) {
|
|
12
|
+
return parseJsonFlag(config, '--config');
|
|
13
|
+
}
|
|
4
14
|
export function validateLoadOption(load) {
|
|
5
15
|
if (load && !['copy', 'symlink', 'move', 'inPlace'].includes(load)) {
|
|
6
|
-
throw new Error('
|
|
16
|
+
throw new Error('--load must be one of: copy, symlink, move, inPlace');
|
|
7
17
|
}
|
|
8
18
|
}
|
|
9
19
|
export function validateTrackArg(track) {
|
|
@@ -21,6 +31,17 @@ export function validateLoadAndLocation(location, load) {
|
|
|
21
31
|
copy/move/symlink/inPlace (inPlace for no file operations)`);
|
|
22
32
|
}
|
|
23
33
|
}
|
|
34
|
+
// --multiwig follows the same local-file rule as a single track: local sources
|
|
35
|
+
// need a --load mode, a pure-URL set must omit it. hasLocalSource is true when
|
|
36
|
+
// any comma/JSON string source is a local path.
|
|
37
|
+
export function validateMultiWiggleLoad(hasLocalSource, load) {
|
|
38
|
+
if (hasLocalSource && !load) {
|
|
39
|
+
throw new Error('--load is required when --multiwig includes local files (copy/move/symlink/inPlace)');
|
|
40
|
+
}
|
|
41
|
+
if (!hasLocalSource && load) {
|
|
42
|
+
throw new Error('The --load flag is used for local files only, but every --multiwig source is a URL');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
24
45
|
export function validateAdapterType(adapterType) {
|
|
25
46
|
if (adapterType === 'UNKNOWN') {
|
|
26
47
|
throw new Error('Track type is not recognized');
|
|
@@ -37,21 +58,16 @@ export function validateAssemblies(configContents, assemblyNames) {
|
|
|
37
58
|
throw new Error('Too many assemblies, cannot default to one. Please specify the assembly with the --assemblyNames flag');
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (subDir) {
|
|
52
|
-
const dir = path.join(configDir, subDir);
|
|
53
|
-
if (!fs.existsSync(dir)) {
|
|
54
|
-
fs.mkdirSync(dir);
|
|
55
|
-
}
|
|
61
|
+
// warn (don't throw) when a --assemblyNames entry matches no assembly in the
|
|
62
|
+
// config: a typo or reversed query,target usually means this, but referencing an
|
|
63
|
+
// assembly to be added later is legitimate, so it stays a soft warning
|
|
64
|
+
export function warnUnknownAssemblyNames(configContents, assemblyNames) {
|
|
65
|
+
const known = new Set(configContents.assemblies?.flatMap(a => [a.name, ...(a.aliases ?? [])]) ??
|
|
66
|
+
[]);
|
|
67
|
+
const missing = parseCommaSeparatedString(assemblyNames).filter(name => !known.has(name));
|
|
68
|
+
if (missing.length) {
|
|
69
|
+
console.warn(`Warning: assembly name(s) not found in config: ${missing.join(', ')}. ` +
|
|
70
|
+
`Available: ${[...known].join(', ')}. ` +
|
|
71
|
+
'Check for a typo or reversed query,target order; the track will not display until a matching assembly exists.');
|
|
56
72
|
}
|
|
57
73
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { parseArgs } from 'util';
|
|
3
|
-
import { printHelp, resolveConfigPath } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { printHelp, readConfigFile, resolveConfigPath } from '../utils.js';
|
|
4
|
+
import { guessTrack, guessTrackType, makeLocationProtocol, } from './add-track-utils/adapter-utils.js';
|
|
5
|
+
import { loadFiles } from './add-track-utils/file-operations.js';
|
|
6
|
+
import { buildMultiWiggle } from './add-track-utils/multiwig.js';
|
|
7
|
+
import { addSyntenyAssemblyNames, buildTrackConfig, mapLocationForFiles, mergeDisplayDefaults, } from './add-track-utils/track-config.js';
|
|
8
|
+
import { parseConfigFlag, validateAdapterType, validateAssemblies, validateLoadAndLocation, validateLoadOption, validateMultiWiggleLoad, validateTrackArg, warnUnknownAssemblyNames, } from './add-track-utils/validators.js';
|
|
9
|
+
import { findAndUpdateOrAdd, saveConfigAndReport, } from './shared/config-operations.js';
|
|
8
10
|
export async function run(args) {
|
|
9
11
|
const options = {
|
|
10
12
|
help: {
|
|
@@ -16,6 +18,10 @@ export async function run(args) {
|
|
|
16
18
|
short: 't',
|
|
17
19
|
description: 'Type of track, by default inferred from track file',
|
|
18
20
|
},
|
|
21
|
+
adapterType: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Adapter type, by default inferred from track file',
|
|
24
|
+
},
|
|
19
25
|
name: {
|
|
20
26
|
type: 'string',
|
|
21
27
|
short: 'n',
|
|
@@ -33,7 +39,7 @@ export async function run(args) {
|
|
|
33
39
|
assemblyNames: {
|
|
34
40
|
type: 'string',
|
|
35
41
|
short: 'a',
|
|
36
|
-
description: 'Assembly name or names for track as comma separated string',
|
|
42
|
+
description: 'Assembly name or names for track as comma separated string. For pairwise synteny tracks the order is query,target (reverse of minimap2/nucmer argument order); for all-vs-all adapters (AllVsAllPAFAdapter/AllVsAllIndexedPAFAdapter) list every assembly the file covers, in any order',
|
|
37
43
|
},
|
|
38
44
|
category: {
|
|
39
45
|
type: 'string',
|
|
@@ -43,6 +49,22 @@ export async function run(args) {
|
|
|
43
49
|
type: 'string',
|
|
44
50
|
description: 'Any extra config settings to add to a track',
|
|
45
51
|
},
|
|
52
|
+
color: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Track color: a plain CSS color or a jexl callback. Merged into displayDefaults',
|
|
55
|
+
},
|
|
56
|
+
height: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Track display height in pixels. Merged into displayDefaults',
|
|
59
|
+
},
|
|
60
|
+
displayDefaults: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Inline JSON merged into the track displayDefaults (labels, mouseover, jexlFilters, etc.)',
|
|
63
|
+
},
|
|
64
|
+
multiwig: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Build a MultiQuantitativeTrack from several BigWigs (in place of the positional track arg): a comma-separated list of BigWig files/URLs, or a .json file holding an array of BigWig locations or subadapter objects, each with its own name/color',
|
|
67
|
+
},
|
|
46
68
|
target: {
|
|
47
69
|
type: 'string',
|
|
48
70
|
description: 'Path to config file in JB2 installation to write out to',
|
|
@@ -62,20 +84,13 @@ export async function run(args) {
|
|
|
62
84
|
load: {
|
|
63
85
|
type: 'string',
|
|
64
86
|
short: 'l',
|
|
65
|
-
description: 'How to manage the track
|
|
66
|
-
|
|
67
|
-
skipCheck: {
|
|
68
|
-
type: 'boolean',
|
|
69
|
-
description: 'Skip check for whether file or URL exists',
|
|
70
|
-
},
|
|
71
|
-
overwrite: {
|
|
72
|
-
type: 'boolean',
|
|
73
|
-
description: 'Overwrites existing track if it shares the same trackId',
|
|
87
|
+
description: 'How to manage the track file relative to config.json. Required for local files, omit for URLs',
|
|
88
|
+
choices: ['copy', 'symlink', 'move', 'inPlace'],
|
|
74
89
|
},
|
|
75
90
|
force: {
|
|
76
91
|
type: 'boolean',
|
|
77
92
|
short: 'f',
|
|
78
|
-
description: '
|
|
93
|
+
description: 'Overwrite existing track and any existing files',
|
|
79
94
|
},
|
|
80
95
|
protocol: {
|
|
81
96
|
type: 'string',
|
|
@@ -96,6 +111,34 @@ export async function run(args) {
|
|
|
96
111
|
allowPositionals: true,
|
|
97
112
|
});
|
|
98
113
|
const description = 'Add a track to a JBrowse 2 configuration';
|
|
114
|
+
const notes = '--load controls how the data file is placed relative to config.json: ' +
|
|
115
|
+
'copy, move, or symlink it into the install directory, or inPlace to ' +
|
|
116
|
+
'reference a pre-staged local file where it already sits. Omit --load ' +
|
|
117
|
+
'entirely for URLs. For a local file the matching index is whichever ' +
|
|
118
|
+
'conventional spelling is actually beside it — <file>.bai/.crai/.tbi, a ' +
|
|
119
|
+
'<file>.csi, or the reads.bai form Picard writes beside reads.bam. A URL ' +
|
|
120
|
+
'cannot be checked, so it gets <file>.bai/.tbi; pass --indexFile for ' +
|
|
121
|
+
'anything else.\n\n' +
|
|
122
|
+
'--config takes inline JSON (not a file path) that is merged into the ' +
|
|
123
|
+
'generated track config, so you can set fields the dedicated flags do not ' +
|
|
124
|
+
'cover, e.g. --config \'{"metadata":{"skipTextIndex":true}}\' to exclude ' +
|
|
125
|
+
'the track from jbrowse text-index.\n\n' +
|
|
126
|
+
'--color and --height set the two most common appearance settings without ' +
|
|
127
|
+
'writing JSON. Wrap the value in single quotes and use double quotes inside ' +
|
|
128
|
+
'a jexl callback so nothing needs escaping, e.g. ' +
|
|
129
|
+
'--color \'jexl:feature.strand==1?"blue":"red"\'. --displayDefaults takes ' +
|
|
130
|
+
'inline JSON for any other appearance setting (labels, mouseover, ' +
|
|
131
|
+
'jexlFilters).\n\n' +
|
|
132
|
+
'--multiwig bundles several BigWigs into one MultiQuantitativeTrack, in ' +
|
|
133
|
+
'place of the positional track argument: pass a comma-separated list of ' +
|
|
134
|
+
'BigWig files/URLs, or a .json file with an array of BigWig locations or ' +
|
|
135
|
+
'subadapter objects (each carrying its own name/color/group). With --load, ' +
|
|
136
|
+
'local list entries are copied like any other track file.\n\n' +
|
|
137
|
+
'For pairwise synteny adapters (PAF/Delta/Chain) --assemblyNames is ' +
|
|
138
|
+
'query,target — the reverse of the minimap2/nucmer input order. For the ' +
|
|
139
|
+
'all-vs-all adapters (AllVsAllPAFAdapter, AllVsAllIndexedPAFAdapter) it is ' +
|
|
140
|
+
'instead the full list of assemblies the file covers, in any order, since ' +
|
|
141
|
+
'one all-vs-all file backs every pair.';
|
|
99
142
|
const examples = [
|
|
100
143
|
'# copy /path/to/my.bam and /path/to/my.bam.bai to current directory and adds track to config.json',
|
|
101
144
|
'$ jbrowse add-track /path/to/my.bam --load copy',
|
|
@@ -114,72 +157,145 @@ export async function run(args) {
|
|
|
114
157
|
'',
|
|
115
158
|
'# --load inPlace adds a track without doing file operations',
|
|
116
159
|
'$ jbrowse add-track /url/relative/path.bam --load inPlace',
|
|
160
|
+
'',
|
|
161
|
+
'# color a track by strand and set its height (no escaping: single-quote the value, double-quote inside the jexl)',
|
|
162
|
+
`$ jbrowse add-track genes.gff3.gz --load copy --color 'jexl:feature.strand==1?"blue":"red"' --height 200`,
|
|
163
|
+
'',
|
|
164
|
+
'# bundle several BigWigs into one MultiQuantitativeTrack (no positional track arg)',
|
|
165
|
+
'$ jbrowse add-track --multiwig a.bw,b.bw,c.bw --load copy --name "Coverage"',
|
|
166
|
+
'',
|
|
167
|
+
'# ...or from a sources.json carrying per-row name/color for each BigWig',
|
|
168
|
+
'$ jbrowse add-track --multiwig sources.json --name "CATlas ATAC"',
|
|
117
169
|
];
|
|
118
170
|
if (flags.help) {
|
|
119
171
|
printHelp({
|
|
120
172
|
description,
|
|
121
173
|
examples,
|
|
174
|
+
notes,
|
|
122
175
|
usage: 'jbrowse add-track <track> [options]',
|
|
123
176
|
options,
|
|
124
177
|
});
|
|
125
178
|
return;
|
|
126
179
|
}
|
|
127
180
|
validateLoadOption(flags.load);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
const { config, force, category, description: trackDescription, load, subDir = '', target, protocol = 'uri', out, indexFile: index, bed1, bed2, adapterType, multiwig, } = flags;
|
|
182
|
+
const location = positionals[0];
|
|
183
|
+
if (multiwig && location) {
|
|
184
|
+
throw new Error('Pass either a positional track file or --multiwig, not both');
|
|
185
|
+
}
|
|
186
|
+
const wrap = makeLocationProtocol(protocol);
|
|
187
|
+
const mapLocation = (p) => wrap(mapLocationForFiles(p, load, subDir));
|
|
188
|
+
// build the adapter (and the set of files to load) up front, so the track-arg
|
|
189
|
+
// and load validation runs before we touch the config on disk
|
|
190
|
+
const { adapter, files, trackType, trackId } = multiwig
|
|
191
|
+
? buildMultiWiggleTrack({
|
|
192
|
+
sources: multiwig,
|
|
193
|
+
mapLocation,
|
|
194
|
+
load,
|
|
195
|
+
trackType: flags.trackType,
|
|
196
|
+
trackId: flags.trackId,
|
|
197
|
+
})
|
|
198
|
+
: buildFileTrack({
|
|
199
|
+
location,
|
|
200
|
+
index,
|
|
201
|
+
bed1,
|
|
202
|
+
bed2,
|
|
203
|
+
adapterType,
|
|
204
|
+
assemblyNames: flags.assemblyNames,
|
|
205
|
+
mapLocation,
|
|
206
|
+
load,
|
|
207
|
+
trackType: flags.trackType,
|
|
208
|
+
trackId: flags.trackId,
|
|
209
|
+
});
|
|
210
|
+
const baseConfigObj = config ? parseConfigFlag(config) : undefined;
|
|
211
|
+
const displayDefaults = mergeDisplayDefaults({
|
|
212
|
+
configObj: baseConfigObj,
|
|
213
|
+
color: flags.color,
|
|
214
|
+
height: flags.height,
|
|
215
|
+
displayDefaults: flags.displayDefaults,
|
|
216
|
+
trackType,
|
|
217
|
+
});
|
|
218
|
+
const configObj = displayDefaults
|
|
219
|
+
? { ...baseConfigObj, displayDefaults }
|
|
220
|
+
: baseConfigObj;
|
|
131
221
|
const targetConfigPath = await resolveConfigPath(target, out);
|
|
132
222
|
const configDir = path.dirname(targetConfigPath);
|
|
133
|
-
|
|
134
|
-
const location = track;
|
|
135
|
-
const mapLoc = (p) => mapLocationForFiles(p, load, subDir);
|
|
136
|
-
let adapter = guessAdapter({
|
|
137
|
-
protocol,
|
|
138
|
-
location: mapLoc(location),
|
|
139
|
-
index: index ? mapLoc(index) : undefined,
|
|
140
|
-
bed1: bed1 ? mapLoc(bed1) : undefined,
|
|
141
|
-
bed2: bed2 ? mapLoc(bed2) : undefined,
|
|
142
|
-
});
|
|
143
|
-
adapter = addSyntenyAssemblyNames(adapter, flags.assemblyNames);
|
|
144
|
-
validateLoadAndLocation(location, load);
|
|
145
|
-
validateAdapterType(adapter.type);
|
|
146
|
-
const configContents = await loadTrackConfig(targetConfigPath);
|
|
223
|
+
const configContents = await readConfigFile(targetConfigPath);
|
|
147
224
|
validateAssemblies(configContents, flags.assemblyNames);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
225
|
+
const name = flags.name || trackId;
|
|
226
|
+
const assemblyNames = flags.assemblyNames || configContents.assemblies?.[0]?.name || '';
|
|
227
|
+
warnUnknownAssemblyNames(configContents, assemblyNames);
|
|
228
|
+
const trackConfig = buildTrackConfig({
|
|
229
|
+
trackType,
|
|
230
|
+
trackId,
|
|
231
|
+
name,
|
|
232
|
+
assemblyNames,
|
|
233
|
+
category,
|
|
234
|
+
description: trackDescription,
|
|
235
|
+
configObj,
|
|
151
236
|
adapter,
|
|
152
|
-
configContents,
|
|
153
237
|
});
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
238
|
+
const { updatedItems: tracks, wasOverwritten } = findAndUpdateOrAdd({
|
|
239
|
+
items: configContents.tracks ?? [],
|
|
240
|
+
newItem: trackConfig,
|
|
241
|
+
idField: 'trackId',
|
|
242
|
+
getId: item => item.trackId,
|
|
243
|
+
force: force ?? false,
|
|
244
|
+
itemType: 'track',
|
|
160
245
|
});
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
246
|
+
const updatedConfig = { ...configContents, tracks };
|
|
247
|
+
await loadFiles({
|
|
248
|
+
files,
|
|
249
|
+
destDir: configDir,
|
|
250
|
+
mode: load,
|
|
251
|
+
subDir,
|
|
165
252
|
force,
|
|
166
|
-
overwrite,
|
|
167
253
|
});
|
|
168
|
-
await
|
|
254
|
+
await saveConfigAndReport({
|
|
255
|
+
config: updatedConfig,
|
|
256
|
+
target: targetConfigPath,
|
|
257
|
+
itemType: 'track',
|
|
258
|
+
itemName: name,
|
|
259
|
+
itemId: trackId,
|
|
260
|
+
wasOverwritten,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// --multiwig: bundle several BigWigs into one MultiQuantitativeTrack
|
|
264
|
+
function buildMultiWiggleTrack({ sources, mapLocation, load, trackType, trackId, }) {
|
|
265
|
+
const { adapter, files } = buildMultiWiggle({ sources, mapLocation });
|
|
266
|
+
validateMultiWiggleLoad(files.length > 0, load);
|
|
267
|
+
return {
|
|
268
|
+
adapter,
|
|
269
|
+
files,
|
|
270
|
+
trackType: trackType || 'MultiQuantitativeTrack',
|
|
271
|
+
trackId: trackId || multiWiggleTrackId(sources),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// the usual path: one positional data file, adapter guessed from its extension
|
|
275
|
+
function buildFileTrack({ location, index, bed1, bed2, adapterType, assemblyNames, mapLocation, load, trackType, trackId, }) {
|
|
276
|
+
validateTrackArg(location);
|
|
277
|
+
validateLoadAndLocation(location, load);
|
|
278
|
+
const { adapter: guessedAdapter, files } = guessTrack({
|
|
169
279
|
location,
|
|
170
280
|
index,
|
|
171
281
|
bed1,
|
|
172
282
|
bed2,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
subDir,
|
|
176
|
-
force,
|
|
177
|
-
});
|
|
178
|
-
await saveTrackConfigAndReport({
|
|
179
|
-
config: updatedConfig,
|
|
180
|
-
targetConfigPath,
|
|
181
|
-
name: trackParams.name,
|
|
182
|
-
trackId: trackParams.trackId,
|
|
183
|
-
wasOverwritten,
|
|
283
|
+
adapterType,
|
|
284
|
+
mapLocation,
|
|
184
285
|
});
|
|
286
|
+
const adapter = addSyntenyAssemblyNames(guessedAdapter, assemblyNames);
|
|
287
|
+
validateAdapterType(adapter.type);
|
|
288
|
+
return {
|
|
289
|
+
adapter,
|
|
290
|
+
files,
|
|
291
|
+
trackType: trackType || guessTrackType(adapter.type, location),
|
|
292
|
+
trackId: trackId || path.basename(location, path.extname(location)),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// a .json sources file names the track after itself (sources.json -> sources); a
|
|
296
|
+
// bare comma list has no filename to borrow, so it falls back to 'multiwiggle'
|
|
297
|
+
function multiWiggleTrackId(sources) {
|
|
298
|
+
return sources.toLowerCase().endsWith('.json')
|
|
299
|
+
? path.basename(sources, path.extname(sources))
|
|
300
|
+
: 'multiwiggle';
|
|
185
301
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import cors from 'cors';
|
|
6
|
-
import express from 'express';
|
|
7
|
-
import { generateKey, parsePort, setupConfigFile, setupRoutes, startServer, } from "./utils.js";
|
|
8
|
-
import { debug, printHelp } from "../../utils.js";
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { printHelp } from '../../utils.js';
|
|
4
|
+
import { createRequestHandler, generateKey, parseByteLimit, parsePort, setupConfigFile, startServer, } from './utils.js';
|
|
9
5
|
export async function run(args) {
|
|
10
6
|
const options = {
|
|
11
7
|
help: {
|
|
@@ -32,11 +28,29 @@ export async function run(args) {
|
|
|
32
28
|
allowPositionals: true,
|
|
33
29
|
});
|
|
34
30
|
const description = 'Start up a small admin server for JBrowse configuration';
|
|
35
|
-
const examples = [
|
|
31
|
+
const examples = [
|
|
32
|
+
'# start the admin server for the JBrowse install in the current directory',
|
|
33
|
+
'$ jbrowse admin-server',
|
|
34
|
+
'',
|
|
35
|
+
'# start on a specific port',
|
|
36
|
+
'$ jbrowse admin-server -p 8888',
|
|
37
|
+
'',
|
|
38
|
+
'# point at a specific JBrowse installation directory',
|
|
39
|
+
'$ jbrowse admin-server --root /path/to/jb2/',
|
|
40
|
+
'',
|
|
41
|
+
'# raise the body size limit for very large config updates',
|
|
42
|
+
'$ jbrowse admin-server --bodySizeLimit 100mb',
|
|
43
|
+
];
|
|
44
|
+
const notes = 'The admin-server lets a browser session write changes back to ' +
|
|
45
|
+
'config.json on disk, authorized by a one-time key printed in the ' +
|
|
46
|
+
'startup URL. It is meant for local configuration only: run it on a ' +
|
|
47
|
+
'trusted machine and do not expose the port to untrusted networks or the ' +
|
|
48
|
+
'public internet.';
|
|
36
49
|
if (flags.help) {
|
|
37
50
|
printHelp({
|
|
38
51
|
description,
|
|
39
52
|
examples,
|
|
53
|
+
notes,
|
|
40
54
|
usage: 'jbrowse admin-server [options]',
|
|
41
55
|
options,
|
|
42
56
|
});
|
|
@@ -44,51 +58,18 @@ export async function run(args) {
|
|
|
44
58
|
}
|
|
45
59
|
const { root, bodySizeLimit = '25mb' } = flags;
|
|
46
60
|
const { outFile, baseDir } = await setupConfigFile({ root });
|
|
47
|
-
// Parse and validate port
|
|
48
61
|
const port = parsePort({ portStr: flags.port });
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// outFile,
|
|
53
|
-
// bodySizeLimit,
|
|
54
|
-
// })
|
|
55
|
-
const app = express();
|
|
56
|
-
// Configure middleware
|
|
57
|
-
app.use(express.static(baseDir));
|
|
58
|
-
app.use(cors());
|
|
59
|
-
app.use(express.json({ limit: bodySizeLimit }));
|
|
60
|
-
// Add error handling middleware
|
|
61
|
-
app.use((err, _req, res, next) => {
|
|
62
|
-
if (err) {
|
|
63
|
-
console.error('Server error:', err);
|
|
64
|
-
res.status(500).setHeader('Content-Type', 'text/plain');
|
|
65
|
-
res.send('Internal Server Error');
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
next();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
// Generate admin key and store it
|
|
62
|
+
// the key lives only in this process and in the startup URL printed below. It
|
|
63
|
+
// used to also be written to os.tmpdir()/jbrowse-admin-<key>, which nothing
|
|
64
|
+
// ever read and which published the key in a filename any local user can list
|
|
72
65
|
const key = generateKey();
|
|
73
|
-
const keyPath = path.join(os.tmpdir(), `jbrowse-admin-${key}`);
|
|
74
|
-
try {
|
|
75
|
-
fs.writeFileSync(keyPath, key);
|
|
76
|
-
debug(`Admin key stored at ${keyPath}`);
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
console.error(`Failed to write admin key to ${keyPath}:`, error instanceof Error ? error.message : error);
|
|
80
|
-
// Continue anyway, as this is not critical
|
|
81
|
-
}
|
|
82
|
-
// Create server reference for shutdown route
|
|
83
66
|
const serverRef = { current: null };
|
|
84
|
-
|
|
85
|
-
setupRoutes({
|
|
86
|
-
app,
|
|
67
|
+
const server = http.createServer(createRequestHandler({
|
|
87
68
|
baseDir,
|
|
88
69
|
outFile,
|
|
89
70
|
key,
|
|
90
71
|
serverRef,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
startServer({
|
|
72
|
+
bodySizeLimit: parseByteLimit(bodySizeLimit),
|
|
73
|
+
}));
|
|
74
|
+
startServer({ server, port, key, outFile, serverRef });
|
|
94
75
|
}
|