@jbrowse/cli 4.3.0 → 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.
- package/README.md +451 -72
- 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 +189 -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 +66 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +200 -62
- package/dist/commands/admin-server/index.js +39 -50
- package/dist/commands/admin-server/utils.js +238 -126
- 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 +133 -11
- package/dist/commands/make-pif/pif-generator.js +243 -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 +19 -26
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +37 -7
- package/dist/commands/text-index/config-utils.js +62 -85
- 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 +44 -96
- 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 +7613 -0
- package/dist/commands/validate/configSchema.generated.js +20400 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +113 -0
- package/dist/commands/validate/liftConfig.js +34 -0
- package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
- package/dist/commands/validate/markRules/autoBin.js +27 -0
- package/dist/commands/validate/markRules/colorScale.js +244 -0
- package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
- package/dist/commands/validate/markRules/jexlStrings.js +23 -0
- package/dist/commands/validate/markRules/markProblems.js +532 -0
- package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
- package/dist/commands/validate/markRules/markSpecs.js +96 -0
- package/dist/commands/validate/markRules/markVocabulary.js +57 -0
- package/dist/commands/validate/markRules/shapeNameList.js +7 -0
- package/dist/commands/validate/markRules/stepChannels.js +55 -0
- package/dist/commands/validate/schemaValidate.js +425 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +502 -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 -14
- package/bin/run +0 -4
- package/bundle/index.js +0 -3686
- 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,42 +1,83 @@
|
|
|
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: expandTrackConfigShorthand drops an unmatched key with a console
|
|
33
|
+
// warning, and the command otherwise wrote the dead setting and exited 0.
|
|
34
|
+
export function mergeDisplayDefaults({ configObj, color, height, displayDefaults, trackType, }) {
|
|
35
|
+
const fromJson = displayDefaults
|
|
36
|
+
? parseJsonFlag(displayDefaults, '--displayDefaults')
|
|
37
|
+
: {};
|
|
38
|
+
const typed = {};
|
|
39
|
+
if (color !== undefined) {
|
|
40
|
+
typed.color = color;
|
|
41
|
+
}
|
|
42
|
+
if (height !== undefined) {
|
|
43
|
+
const asNumber = Number(height);
|
|
44
|
+
typed.height = Number.isNaN(asNumber) ? height : asNumber;
|
|
45
|
+
}
|
|
46
|
+
const existing = (configObj?.displayDefaults ?? {});
|
|
47
|
+
const merged = { ...existing, ...fromJson, ...typed };
|
|
48
|
+
if (Object.keys(merged).length === 0) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
checkDisplayDefaultKeys(merged, trackType);
|
|
52
|
+
return merged;
|
|
53
|
+
}
|
|
54
|
+
// An unknown track type accepts everything: it is a plugin's, and this manifest
|
|
55
|
+
// is only the core plugins'.
|
|
56
|
+
//
|
|
57
|
+
// The message names the display where the track offers exactly one, and the
|
|
58
|
+
// track otherwise: the keys checked are the union over its displays, so
|
|
59
|
+
// naming one of several would send the reader to look in the wrong place.
|
|
60
|
+
function checkDisplayDefaultKeys(merged, trackType) {
|
|
61
|
+
const { displayTypes, keys } = displayDefaultsForTrackType(trackType);
|
|
62
|
+
if (keys.length === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const unknown = Object.keys(merged).filter(key => !keys.includes(key));
|
|
66
|
+
if (unknown.length > 0) {
|
|
67
|
+
const declarer = displayTypes.length === 1 ? displayTypes[0] : `any ${trackType} display`;
|
|
68
|
+
throw new Error(unknown
|
|
69
|
+
.map(key => {
|
|
70
|
+
const hit = suggest(key, keys);
|
|
71
|
+
return `"${key}" is not a setting ${declarer} declares${hit ? `, did you mean "${hit}"?` : ''}`;
|
|
72
|
+
})
|
|
73
|
+
.join('\n'));
|
|
74
|
+
}
|
|
34
75
|
}
|
|
35
76
|
export function addSyntenyAssemblyNames(adapter, assemblyNames) {
|
|
36
|
-
if (
|
|
77
|
+
if (syntenyAdapterTypes.has(adapter.type)) {
|
|
37
78
|
return {
|
|
38
79
|
...adapter,
|
|
39
|
-
assemblyNames: assemblyNames
|
|
80
|
+
assemblyNames: parseCommaSeparatedString(assemblyNames),
|
|
40
81
|
};
|
|
41
82
|
}
|
|
42
83
|
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, withDensityAdapter, } 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 the multi-genome PAF adapters (MultiGenomePAFAdapter/MultiGenomeIndexedPAFAdapter) 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, filter, 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',
|
|
@@ -89,6 +104,10 @@ export async function run(args) {
|
|
|
89
104
|
type: 'string',
|
|
90
105
|
description: 'Used only for mcscan anchors/simpleAnchors types',
|
|
91
106
|
},
|
|
107
|
+
density: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: "Path or URL of a features-per-bin bigWig (jbrowse make-density) to attach as the adapter's densityAdapter. A local <file>.density.bw beside the track file is attached without this flag",
|
|
110
|
+
},
|
|
92
111
|
};
|
|
93
112
|
const { values: flags, positionals } = parseArgs({
|
|
94
113
|
args,
|
|
@@ -96,6 +115,40 @@ export async function run(args) {
|
|
|
96
115
|
allowPositionals: true,
|
|
97
116
|
});
|
|
98
117
|
const description = 'Add a track to a JBrowse 2 configuration';
|
|
118
|
+
const notes = '--load controls how the data file is placed relative to config.json: ' +
|
|
119
|
+
'copy, move, or symlink it into the install directory, or inPlace to ' +
|
|
120
|
+
'reference a pre-staged local file where it already sits. Omit --load ' +
|
|
121
|
+
'entirely for URLs. For a local file the matching index is whichever ' +
|
|
122
|
+
'conventional spelling is actually beside it — <file>.bai/.crai/.tbi, a ' +
|
|
123
|
+
'<file>.csi, or the reads.bai form Picard writes beside reads.bam. A URL ' +
|
|
124
|
+
'cannot be checked, so it gets <file>.bai/.tbi; pass --indexFile for ' +
|
|
125
|
+
'anything else.\n\n' +
|
|
126
|
+
'--config takes inline JSON (not a file path) that is merged into the ' +
|
|
127
|
+
'generated track config, so you can set fields the dedicated flags do not ' +
|
|
128
|
+
'cover, e.g. --config \'{"metadata":{"skipTextIndex":true}}\' to exclude ' +
|
|
129
|
+
'the track from jbrowse text-index.\n\n' +
|
|
130
|
+
'--color and --height set the two most common appearance settings without ' +
|
|
131
|
+
'writing JSON. Wrap the value in single quotes and use double quotes inside ' +
|
|
132
|
+
'a jexl callback so nothing needs escaping, e.g. ' +
|
|
133
|
+
'--color \'jexl:feature.strand==1?"blue":"red"\'. --displayDefaults takes ' +
|
|
134
|
+
'inline JSON for any other appearance setting (labels, mouseover, ' +
|
|
135
|
+
'filter).\n\n' +
|
|
136
|
+
'--multiwig bundles several BigWigs into one MultiQuantitativeTrack, in ' +
|
|
137
|
+
'place of the positional track argument: pass a comma-separated list of ' +
|
|
138
|
+
'BigWig files/URLs, or a .json file with an array of BigWig locations or ' +
|
|
139
|
+
'subadapter objects (each carrying its own name/color/group). With --load, ' +
|
|
140
|
+
'local list entries are copied like any other track file.\n\n' +
|
|
141
|
+
'--density attaches a features-per-bin bigWig (jbrowse make-density) as the ' +
|
|
142
|
+
"adapter's densityAdapter, the band a display draws where the region is too " +
|
|
143
|
+
'large to fetch features. A local <file>.density.bw beside the track file is ' +
|
|
144
|
+
'attached without the flag; a URL cannot be probed, so a remote sidecar needs ' +
|
|
145
|
+
'it. The adapters carrying the slot are BamAdapter, CramAdapter, ' +
|
|
146
|
+
'HtsgetBamAdapter, Gff3TabixAdapter, GtfTabixAdapter, BedTabixAdapter, ' +
|
|
147
|
+
'BigBedAdapter, VcfTabixAdapter and SplitVcfTabixAdapter.\n\n' +
|
|
148
|
+
'For pairwise synteny adapters (PAF/Delta/Chain) --assemblyNames is ' +
|
|
149
|
+
'query,target — the reverse of the minimap2/nucmer input order. For the ' +
|
|
150
|
+
'multi-genome PAF adapters (MultiGenomePAFAdapter, MultiGenomeIndexedPAFAdapter) it is ' +
|
|
151
|
+
'instead the full list of assemblies the file covers, in any order.';
|
|
99
152
|
const examples = [
|
|
100
153
|
'# copy /path/to/my.bam and /path/to/my.bam.bai to current directory and adds track to config.json',
|
|
101
154
|
'$ jbrowse add-track /path/to/my.bam --load copy',
|
|
@@ -114,72 +167,157 @@ export async function run(args) {
|
|
|
114
167
|
'',
|
|
115
168
|
'# --load inPlace adds a track without doing file operations',
|
|
116
169
|
'$ jbrowse add-track /url/relative/path.bam --load inPlace',
|
|
170
|
+
'',
|
|
171
|
+
'# color a track by strand and set its height (no escaping: single-quote the value, double-quote inside the jexl)',
|
|
172
|
+
`$ jbrowse add-track genes.gff3.gz --load copy --color 'jexl:feature.strand==1?"blue":"red"' --height 200`,
|
|
173
|
+
'',
|
|
174
|
+
'# bundle several BigWigs into one MultiQuantitativeTrack (no positional track arg)',
|
|
175
|
+
'$ jbrowse add-track --multiwig a.bw,b.bw,c.bw --load copy --name "Coverage"',
|
|
176
|
+
'',
|
|
177
|
+
'# ...or from a sources.json carrying per-row name/color for each BigWig',
|
|
178
|
+
'$ jbrowse add-track --multiwig sources.json --name "CATlas ATAC"',
|
|
179
|
+
'',
|
|
180
|
+
'# attach a remote density sidecar (a local genes.gff3.density.bw needs no flag)',
|
|
181
|
+
'$ jbrowse add-track https://mywebsite.com/genes.gff3.gz --density https://mywebsite.com/genes.gff3.density.bw',
|
|
117
182
|
];
|
|
118
183
|
if (flags.help) {
|
|
119
184
|
printHelp({
|
|
120
185
|
description,
|
|
121
186
|
examples,
|
|
187
|
+
notes,
|
|
122
188
|
usage: 'jbrowse add-track <track> [options]',
|
|
123
189
|
options,
|
|
124
190
|
});
|
|
125
191
|
return;
|
|
126
192
|
}
|
|
127
193
|
validateLoadOption(flags.load);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
const { config, force, category, description: trackDescription, load, subDir = '', target, protocol = 'uri', out, indexFile: index, bed1, bed2, adapterType, multiwig, } = flags;
|
|
195
|
+
const location = positionals[0];
|
|
196
|
+
if (multiwig && location) {
|
|
197
|
+
throw new Error('Pass either a positional track file or --multiwig, not both');
|
|
198
|
+
}
|
|
199
|
+
const wrap = makeLocationProtocol(protocol);
|
|
200
|
+
const mapLocation = (p) => wrap(mapLocationForFiles(p, load, subDir));
|
|
201
|
+
// build the adapter (and the set of files to load) up front, so the track-arg
|
|
202
|
+
// and load validation runs before we touch the config on disk
|
|
203
|
+
const { adapter: builtAdapter, files, trackType, trackId, } = multiwig
|
|
204
|
+
? buildMultiWiggleTrack({
|
|
205
|
+
sources: multiwig,
|
|
206
|
+
mapLocation,
|
|
207
|
+
load,
|
|
208
|
+
trackType: flags.trackType,
|
|
209
|
+
trackId: flags.trackId,
|
|
210
|
+
})
|
|
211
|
+
: buildFileTrack({
|
|
212
|
+
location,
|
|
213
|
+
index,
|
|
214
|
+
bed1,
|
|
215
|
+
bed2,
|
|
216
|
+
adapterType,
|
|
217
|
+
assemblyNames: flags.assemblyNames,
|
|
218
|
+
mapLocation,
|
|
219
|
+
load,
|
|
220
|
+
trackType: flags.trackType,
|
|
221
|
+
trackId: flags.trackId,
|
|
222
|
+
});
|
|
223
|
+
const { adapter, file: densityFile } = withDensityAdapter({
|
|
224
|
+
adapter: builtAdapter,
|
|
225
|
+
location,
|
|
226
|
+
density: flags.density,
|
|
227
|
+
makeLocation: mapLocation,
|
|
228
|
+
});
|
|
229
|
+
if (densityFile !== undefined && !load) {
|
|
230
|
+
throw new Error(`--density names a local file (${densityFile}); pass --load to say how it is placed beside the config`);
|
|
231
|
+
}
|
|
232
|
+
const baseConfigObj = config ? parseConfigFlag(config) : undefined;
|
|
233
|
+
const displayDefaults = mergeDisplayDefaults({
|
|
234
|
+
configObj: baseConfigObj,
|
|
235
|
+
color: flags.color,
|
|
236
|
+
height: flags.height,
|
|
237
|
+
displayDefaults: flags.displayDefaults,
|
|
238
|
+
trackType,
|
|
239
|
+
});
|
|
240
|
+
const configObj = displayDefaults
|
|
241
|
+
? { ...baseConfigObj, displayDefaults }
|
|
242
|
+
: baseConfigObj;
|
|
131
243
|
const targetConfigPath = await resolveConfigPath(target, out);
|
|
132
244
|
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);
|
|
245
|
+
const configContents = await readConfigFile(targetConfigPath);
|
|
147
246
|
validateAssemblies(configContents, flags.assemblyNames);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
247
|
+
const name = flags.name || trackId;
|
|
248
|
+
const assemblyNames = flags.assemblyNames || configContents.assemblies?.[0]?.name || '';
|
|
249
|
+
warnUnknownAssemblyNames(configContents, assemblyNames);
|
|
250
|
+
const trackConfig = buildTrackConfig({
|
|
251
|
+
trackType,
|
|
252
|
+
trackId,
|
|
253
|
+
name,
|
|
254
|
+
assemblyNames,
|
|
255
|
+
category,
|
|
256
|
+
description: trackDescription,
|
|
257
|
+
configObj,
|
|
151
258
|
adapter,
|
|
152
|
-
configContents,
|
|
153
259
|
});
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
260
|
+
const { updatedItems: tracks, wasOverwritten } = findAndUpdateOrAdd({
|
|
261
|
+
items: configContents.tracks ?? [],
|
|
262
|
+
newItem: trackConfig,
|
|
263
|
+
idField: 'trackId',
|
|
264
|
+
getId: item => item.trackId,
|
|
265
|
+
force: force ?? false,
|
|
266
|
+
itemType: 'track',
|
|
160
267
|
});
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
268
|
+
const updatedConfig = { ...configContents, tracks };
|
|
269
|
+
await loadFiles({
|
|
270
|
+
files: [...files, densityFile],
|
|
271
|
+
destDir: configDir,
|
|
272
|
+
mode: load,
|
|
273
|
+
subDir,
|
|
165
274
|
force,
|
|
166
|
-
overwrite,
|
|
167
275
|
});
|
|
168
|
-
await
|
|
276
|
+
await saveConfigAndReport({
|
|
277
|
+
config: updatedConfig,
|
|
278
|
+
target: targetConfigPath,
|
|
279
|
+
itemType: 'track',
|
|
280
|
+
itemName: name,
|
|
281
|
+
itemId: trackId,
|
|
282
|
+
wasOverwritten,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// --multiwig: bundle several BigWigs into one MultiQuantitativeTrack
|
|
286
|
+
function buildMultiWiggleTrack({ sources, mapLocation, load, trackType, trackId, }) {
|
|
287
|
+
const { adapter, files } = buildMultiWiggle({ sources, mapLocation });
|
|
288
|
+
validateMultiWiggleLoad(files.length > 0, load);
|
|
289
|
+
return {
|
|
290
|
+
adapter,
|
|
291
|
+
files,
|
|
292
|
+
trackType: trackType || 'MultiQuantitativeTrack',
|
|
293
|
+
trackId: trackId || multiWiggleTrackId(sources),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// the usual path: one positional data file, adapter guessed from its extension
|
|
297
|
+
function buildFileTrack({ location, index, bed1, bed2, adapterType, assemblyNames, mapLocation, load, trackType, trackId, }) {
|
|
298
|
+
validateTrackArg(location);
|
|
299
|
+
validateLoadAndLocation(location, load);
|
|
300
|
+
const { adapter: guessedAdapter, files } = guessTrack({
|
|
169
301
|
location,
|
|
170
302
|
index,
|
|
171
303
|
bed1,
|
|
172
304
|
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,
|
|
305
|
+
adapterType,
|
|
306
|
+
mapLocation,
|
|
184
307
|
});
|
|
308
|
+
const adapter = addSyntenyAssemblyNames(guessedAdapter, assemblyNames);
|
|
309
|
+
validateAdapterType(adapter.type);
|
|
310
|
+
return {
|
|
311
|
+
adapter,
|
|
312
|
+
files,
|
|
313
|
+
trackType: trackType || guessTrackType(adapter.type, location),
|
|
314
|
+
trackId: trackId || path.basename(location, path.extname(location)),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// a .json sources file names the track after itself (sources.json -> sources); a
|
|
318
|
+
// bare comma list has no filename to borrow, so it falls back to 'multiwiggle'
|
|
319
|
+
function multiWiggleTrackId(sources) {
|
|
320
|
+
return sources.toLowerCase().endsWith('.json')
|
|
321
|
+
? path.basename(sources, path.extname(sources))
|
|
322
|
+
: 'multiwiggle';
|
|
185
323
|
}
|
|
@@ -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: {
|
|
@@ -17,6 +13,11 @@ export async function run(args) {
|
|
|
17
13
|
short: 'p',
|
|
18
14
|
description: 'Specified port to start the server on (default: 9090)',
|
|
19
15
|
},
|
|
16
|
+
host: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Interface to bind to (default: 127.0.0.1). Use 0.0.0.0 to accept ' +
|
|
19
|
+
'connections from other machines',
|
|
20
|
+
},
|
|
20
21
|
root: {
|
|
21
22
|
type: 'string',
|
|
22
23
|
description: 'Path to the root of the JB2 installation',
|
|
@@ -32,63 +33,51 @@ export async function run(args) {
|
|
|
32
33
|
allowPositionals: true,
|
|
33
34
|
});
|
|
34
35
|
const description = 'Start up a small admin server for JBrowse configuration';
|
|
35
|
-
const examples = [
|
|
36
|
+
const examples = [
|
|
37
|
+
'# start the admin server for the JBrowse install in the current directory',
|
|
38
|
+
'$ jbrowse admin-server',
|
|
39
|
+
'',
|
|
40
|
+
'# start on a specific port',
|
|
41
|
+
'$ jbrowse admin-server -p 8888',
|
|
42
|
+
'',
|
|
43
|
+
'# point at a specific JBrowse installation directory',
|
|
44
|
+
'$ jbrowse admin-server --root /path/to/jb2/',
|
|
45
|
+
'',
|
|
46
|
+
'# raise the body size limit for very large config updates',
|
|
47
|
+
'$ jbrowse admin-server --bodySizeLimit 100mb',
|
|
48
|
+
'',
|
|
49
|
+
'# accept connections from other machines on the network',
|
|
50
|
+
'$ jbrowse admin-server --host 0.0.0.0',
|
|
51
|
+
];
|
|
52
|
+
const notes = 'The admin-server lets a browser session write changes back to ' +
|
|
53
|
+
'config.json on disk, authorized by a one-time key printed in the ' +
|
|
54
|
+
'startup URL. It binds to 127.0.0.1 so only this machine can reach it. ' +
|
|
55
|
+
'The key gates the config routes, but the rest of the JBrowse directory ' +
|
|
56
|
+
'is served unauthenticated, so widen --host only on a trusted network.';
|
|
36
57
|
if (flags.help) {
|
|
37
58
|
printHelp({
|
|
38
59
|
description,
|
|
39
60
|
examples,
|
|
61
|
+
notes,
|
|
40
62
|
usage: 'jbrowse admin-server [options]',
|
|
41
63
|
options,
|
|
42
64
|
});
|
|
43
65
|
return;
|
|
44
66
|
}
|
|
45
|
-
const { root, bodySizeLimit = '25mb' } = flags;
|
|
67
|
+
const { root, host, bodySizeLimit = '25mb' } = flags;
|
|
46
68
|
const { outFile, baseDir } = await setupConfigFile({ root });
|
|
47
|
-
// Parse and validate port
|
|
48
69
|
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
|
|
70
|
+
// the key lives only in this process and in the startup URL printed below. It
|
|
71
|
+
// used to also be written to os.tmpdir()/jbrowse-admin-<key>, which nothing
|
|
72
|
+
// ever read and which published the key in a filename any local user can list
|
|
72
73
|
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
74
|
const serverRef = { current: null };
|
|
84
|
-
|
|
85
|
-
setupRoutes({
|
|
86
|
-
app,
|
|
75
|
+
const server = http.createServer(createRequestHandler({
|
|
87
76
|
baseDir,
|
|
88
77
|
outFile,
|
|
89
78
|
key,
|
|
90
79
|
serverRef,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
startServer({
|
|
80
|
+
bodySizeLimit: parseByteLimit(bodySizeLimit),
|
|
81
|
+
}));
|
|
82
|
+
startServer({ server, port, host, key, outFile, serverRef });
|
|
94
83
|
}
|