@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.
Files changed (71) hide show
  1. package/README.md +451 -72
  2. package/dist/base.js +0 -3
  3. package/dist/bin.js +2 -1
  4. package/dist/cliFetch.js +3 -0
  5. package/dist/commands/add-assembly/index.js +46 -19
  6. package/dist/commands/add-assembly/utils.js +204 -290
  7. package/dist/commands/add-connection.js +73 -79
  8. package/dist/commands/add-track-json.js +24 -12
  9. package/dist/commands/add-track-utils/adapter-utils.js +189 -288
  10. package/dist/commands/add-track-utils/file-operations.js +27 -11
  11. package/dist/commands/add-track-utils/multiwig.js +47 -0
  12. package/dist/commands/add-track-utils/track-config.js +66 -25
  13. package/dist/commands/add-track-utils/validators.js +36 -20
  14. package/dist/commands/add-track.js +200 -62
  15. package/dist/commands/admin-server/index.js +39 -50
  16. package/dist/commands/admin-server/utils.js +238 -126
  17. package/dist/commands/create.js +18 -34
  18. package/dist/commands/make-density/density-generator.js +180 -0
  19. package/dist/commands/make-density/index.js +139 -0
  20. package/dist/commands/make-pif/index.js +133 -11
  21. package/dist/commands/make-pif/pif-generator.js +243 -43
  22. package/dist/commands/process-utils.js +13 -2
  23. package/dist/commands/remove-track.js +20 -13
  24. package/dist/commands/set-default-session.js +35 -31
  25. package/dist/commands/shared/config-operations.js +3 -10
  26. package/dist/commands/shared/density.js +12 -0
  27. package/dist/commands/shared/sort-utils.js +67 -10
  28. package/dist/commands/shared/validators.js +38 -3
  29. package/dist/commands/sort-bed.js +2 -33
  30. package/dist/commands/sort-gff.js +2 -33
  31. package/dist/commands/text-index/adapter-utils.js +19 -26
  32. package/dist/commands/text-index/aggregate.js +40 -34
  33. package/dist/commands/text-index/command.js +37 -7
  34. package/dist/commands/text-index/config-utils.js +62 -85
  35. package/dist/commands/text-index/file-list.js +32 -18
  36. package/dist/commands/text-index/index.js +4 -4
  37. package/dist/commands/text-index/indexing-utils.js +44 -96
  38. package/dist/commands/text-index/per-track.js +44 -31
  39. package/dist/commands/text-index/validators.js +1 -1
  40. package/dist/commands/upgrade.js +20 -32
  41. package/dist/commands/validate/configManifest.generated.js +7613 -0
  42. package/dist/commands/validate/configSchema.generated.js +20400 -0
  43. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  44. package/dist/commands/validate/index.js +113 -0
  45. package/dist/commands/validate/liftConfig.js +34 -0
  46. package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
  47. package/dist/commands/validate/markRules/autoBin.js +27 -0
  48. package/dist/commands/validate/markRules/colorScale.js +244 -0
  49. package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
  50. package/dist/commands/validate/markRules/jexlStrings.js +23 -0
  51. package/dist/commands/validate/markRules/markProblems.js +532 -0
  52. package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
  53. package/dist/commands/validate/markRules/markSpecs.js +96 -0
  54. package/dist/commands/validate/markRules/markVocabulary.js +57 -0
  55. package/dist/commands/validate/markRules/shapeNameList.js +7 -0
  56. package/dist/commands/validate/markRules/stepChannels.js +55 -0
  57. package/dist/commands/validate/schemaValidate.js +425 -0
  58. package/dist/commands/validate/suggest.js +35 -0
  59. package/dist/commands/validate/types.js +6 -0
  60. package/dist/commands/validate/validateConfig.js +502 -0
  61. package/dist/index.js +104 -50
  62. package/dist/types/common.js +3 -27
  63. package/dist/utils.js +309 -128
  64. package/dist/version.js +1 -1
  65. package/package.json +8 -14
  66. package/bin/run +0 -4
  67. package/bundle/index.js +0 -3686
  68. package/dist/commands/make-pif/cigar-utils.js +0 -24
  69. package/dist/commands/make-pif/file-utils.js +0 -30
  70. package/dist/commands/track-utils.js +0 -74
  71. package/dist/util.js +0 -35
@@ -1,42 +1,83 @@
1
- import path from 'path';
2
- import parseJSON from 'json-parse-better-errors';
3
- import { isURL } from "../../types/common.js";
4
- const SYNTENY_ADAPTERS = new Set([
5
- 'PAFAdapter',
6
- 'PairwiseIndexedPAFAdapter',
7
- 'DeltaAdapter',
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({ location, trackType, trackId, name, assemblyNames, category, description, config, adapter, configContents, }) {
19
- const configObj = config ? parseJSON(config) : {};
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: finalTrackId,
26
- name: finalName,
16
+ trackId,
17
+ name,
27
18
  adapter,
28
- category: category?.split(',').map(c => c.trim()),
29
- assemblyNames: finalAssemblyNames.split(',').map(a => a.trim()),
19
+ category: category ? parseCommaSeparatedString(category) : undefined,
20
+ assemblyNames: parseCommaSeparatedString(assemblyNames),
30
21
  description,
31
22
  ...configObj,
32
23
  };
33
- return trackConfig;
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 (SYNTENY_ADAPTERS.has(adapter.type)) {
77
+ if (syntenyAdapterTypes.has(adapter.type)) {
37
78
  return {
38
79
  ...adapter,
39
- assemblyNames: assemblyNames?.split(',').map(a => a.trim()),
80
+ assemblyNames: parseCommaSeparatedString(assemblyNames),
40
81
  };
41
82
  }
42
83
  return adapter;
@@ -1,9 +1,19 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { isURL } from "../../types/common.js";
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('Error: --load must be one of: copy, symlink, move, inPlace');
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
- export function validateTrackId(configContents, trackId, force, overwrite) {
41
- if (!configContents.tracks) {
42
- configContents.tracks = [];
43
- }
44
- const idx = configContents.tracks.findIndex(c => c.trackId === trackId);
45
- if (idx !== -1 && !force && !overwrite) {
46
- throw new Error(`Cannot add track with id ${trackId}, a track with that id already exists (use --force to override)`);
47
- }
48
- return idx;
49
- }
50
- export function createTargetDirectory(configDir, subDir) {
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 "../utils.js";
4
- import { guessAdapter } from "./add-track-utils/adapter-utils.js";
5
- import { addSyntenyAssemblyNames, mapLocationForFiles, } from "./add-track-utils/track-config.js";
6
- import { createTargetDirectory, validateAdapterType, validateAssemblies, validateLoadAndLocation, validateLoadOption, validateTrackArg, } from "./add-track-utils/validators.js";
7
- import { addTrackToConfig, buildTrackParams, createTrackConfiguration, loadTrackConfig, processTrackFiles, saveTrackConfigAndReport, } from "./track-utils.js";
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 (copy, symlink, move, inPlace)',
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: 'Equivalent to --skipCheck --overwrite',
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 track = positionals[0];
129
- validateTrackArg(track);
130
- const { config, force, overwrite, category, description: trackDescription, load, subDir = '', target, protocol = 'uri', out, indexFile: index, bed1, bed2, } = flags;
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
- createTargetDirectory(configDir, subDir);
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 trackParams = buildTrackParams({
149
- flags,
150
- location,
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 trackConfig = createTrackConfiguration({
155
- location,
156
- trackParams,
157
- flags: { category, description: trackDescription, config },
158
- adapter,
159
- configContents,
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 { updatedConfig, wasOverwritten } = addTrackToConfig({
162
- configContents,
163
- trackConfig,
164
- trackId: trackParams.trackId,
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 processTrackFiles({
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
- load,
174
- configDir,
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 fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { parseArgs } from 'util';
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 = ['$ jbrowse admin-server', '$ jbrowse admin-server -p 8888'];
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
- // Set up the Express server
50
- // const { app, key, keyPath, serverRef } = setupServer({
51
- // baseDir,
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
- // Set up routes
85
- setupRoutes({
86
- app,
75
+ const server = http.createServer(createRequestHandler({
87
76
  baseDir,
88
77
  outFile,
89
78
  key,
90
79
  serverRef,
91
- });
92
- // Start the server and set up shutdown handlers
93
- startServer({ app, port, key, outFile, keyPath, serverRef });
80
+ bodySizeLimit: parseByteLimit(bodySizeLimit),
81
+ }));
82
+ startServer({ server, port, host, key, outFile, serverRef });
94
83
  }