@jbrowse/cli 4.2.1 → 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.
Files changed (54) hide show
  1. package/README.md +334 -60
  2. package/bin/run +1 -1
  3. package/bundle/index.js +493 -2555
  4. package/dist/base.js +0 -3
  5. package/dist/bin.js +2 -1
  6. package/dist/cliFetch.js +3 -0
  7. package/dist/commands/add-assembly/index.js +46 -19
  8. package/dist/commands/add-assembly/utils.js +204 -290
  9. package/dist/commands/add-connection.js +73 -79
  10. package/dist/commands/add-track-json.js +24 -12
  11. package/dist/commands/add-track-utils/adapter-utils.js +173 -283
  12. package/dist/commands/add-track-utils/file-operations.js +27 -11
  13. package/dist/commands/add-track-utils/multiwig.js +47 -0
  14. package/dist/commands/add-track-utils/track-config.js +68 -25
  15. package/dist/commands/add-track-utils/validators.js +36 -20
  16. package/dist/commands/add-track.js +178 -62
  17. package/dist/commands/admin-server/index.js +30 -49
  18. package/dist/commands/admin-server/utils.js +235 -125
  19. package/dist/commands/create.js +18 -34
  20. package/dist/commands/make-pif/cigar-utils.js +62 -19
  21. package/dist/commands/make-pif/index.js +98 -11
  22. package/dist/commands/make-pif/pif-generator.js +276 -42
  23. package/dist/commands/process-utils.js +13 -2
  24. package/dist/commands/remove-track.js +20 -13
  25. package/dist/commands/set-default-session.js +35 -31
  26. package/dist/commands/shared/config-operations.js +3 -10
  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 +6 -23
  32. package/dist/commands/text-index/aggregate.js +40 -34
  33. package/dist/commands/text-index/command.js +35 -7
  34. package/dist/commands/text-index/config-utils.js +60 -83
  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 +49 -73
  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 +5394 -0
  42. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  43. package/dist/commands/validate/index.js +97 -0
  44. package/dist/commands/validate/suggest.js +35 -0
  45. package/dist/commands/validate/types.js +6 -0
  46. package/dist/commands/validate/validateConfig.js +457 -0
  47. package/dist/index.js +99 -50
  48. package/dist/types/common.js +3 -27
  49. package/dist/utils.js +309 -128
  50. package/dist/version.js +1 -1
  51. package/package.json +8 -10
  52. package/dist/commands/make-pif/file-utils.js +0 -30
  53. package/dist/commands/track-utils.js +0 -74
  54. package/dist/util.js +0 -35
@@ -1,42 +1,85 @@
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. `--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 (SYNTENY_ADAPTERS.has(adapter.type)) {
79
+ if (syntenyAdapterTypes.has(adapter.type)) {
37
80
  return {
38
81
  ...adapter,
39
- assemblyNames: assemblyNames?.split(',').map(a => a.trim()),
82
+ assemblyNames: parseCommaSeparatedString(assemblyNames),
40
83
  };
41
84
  }
42
85
  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, } 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 (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',
@@ -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 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;
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
- 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);
223
+ const configContents = await readConfigFile(targetConfigPath);
147
224
  validateAssemblies(configContents, flags.assemblyNames);
148
- const trackParams = buildTrackParams({
149
- flags,
150
- location,
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 trackConfig = createTrackConfiguration({
155
- location,
156
- trackParams,
157
- flags: { category, description: trackDescription, config },
158
- adapter,
159
- configContents,
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 { updatedConfig, wasOverwritten } = addTrackToConfig({
162
- configContents,
163
- trackConfig,
164
- trackId: trackParams.trackId,
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 processTrackFiles({
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
- 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,
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 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: {
@@ -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 = ['$ jbrowse admin-server', '$ jbrowse admin-server -p 8888'];
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
- // 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
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
- // Set up routes
85
- setupRoutes({
86
- app,
67
+ const server = http.createServer(createRequestHandler({
87
68
  baseDir,
88
69
  outFile,
89
70
  key,
90
71
  serverRef,
91
- });
92
- // Start the server and set up shutdown handlers
93
- startServer({ app, port, key, outFile, keyPath, serverRef });
72
+ bodySizeLimit: parseByteLimit(bodySizeLimit),
73
+ }));
74
+ startServer({ server, port, key, outFile, serverRef });
94
75
  }