@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,114 +1,91 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { supported } from "../../types/common.js";
4
- /**
5
- * Parses a comma-separated string into an array of trimmed, non-empty strings
6
- */
7
- export function parseCommaSeparatedString(value) {
8
- return (value
9
- ?.split(',')
10
- .map(s => s.trim())
11
- .filter(Boolean) ?? []);
12
- }
13
- /**
14
- * Validates and parses a prefix size value
15
- */
16
- export function validatePrefixSize(value) {
1
+ import path from 'node:path';
2
+ import { supported } from '../../types/common.js';
3
+ import { parseCommaSeparatedString, readConfigFile, resolveConfigPath, } from '../../utils.js';
4
+ function validatePrefixSize(value) {
17
5
  if (!value) {
18
6
  return undefined;
19
7
  }
20
8
  const parsed = typeof value === 'number' ? value : parseInt(value, 10);
21
- if (isNaN(parsed) || parsed < 0) {
9
+ if (Number.isNaN(parsed) || parsed <= 0) {
22
10
  throw new Error(`Invalid prefixSize: "${value}". Must be a positive number.`);
23
11
  }
24
12
  return parsed;
25
13
  }
26
- /**
27
- * Prepares common indexDriver parameters from raw flag values
28
- */
29
14
  export function prepareIndexDriverFlags(flags) {
30
15
  return {
31
16
  attributes: parseCommaSeparatedString(flags.attributes),
32
17
  typesToExclude: parseCommaSeparatedString(flags.exclude),
18
+ // undefined rather than [] when the flag is absent, because an empty allow
19
+ // list has to mean "no allow list" and not "index nothing"
20
+ typesToInclude: flags.include
21
+ ? parseCommaSeparatedString(flags.include)
22
+ : undefined,
33
23
  quiet: flags.quiet ?? false,
34
24
  prefixSize: validatePrefixSize(flags.prefixSize),
35
25
  };
36
26
  }
37
- export function readConf(configPath) {
38
- return JSON.parse(fs.readFileSync(configPath, 'utf8'));
39
- }
40
- export function writeConf(obj, configPath) {
41
- fs.writeFileSync(configPath, JSON.stringify(obj, null, 2));
42
- }
43
- /**
44
- * Loads config and prepares output location for indexing
45
- */
46
- export async function loadConfigForIndexing(target, out, resolveConfigPath) {
27
+ export async function loadConfigForIndexing(target, out) {
47
28
  const configPath = await resolveConfigPath(target, out);
48
- const outLocation = path.dirname(configPath);
49
- const config = readConf(configPath);
50
- ensureTrixDir(outLocation);
51
- return { config, configPath, outLocation };
52
- }
53
- export function ensureTrixDir(outLocation) {
54
- const trixDir = path.join(outLocation, 'trix');
55
- if (!fs.existsSync(trixDir)) {
56
- fs.mkdirSync(trixDir, { recursive: true });
57
- }
58
- return trixDir;
59
- }
60
- /**
61
- * Extracts all assembly names from a config object
62
- * Handles both single assembly (config.assembly) and multiple assemblies (config.assemblies)
63
- */
64
- function extractAssemblyNamesFromConfig(config) {
65
- if (config.assemblies) {
66
- return config.assemblies.map(a => a.name);
67
- }
68
- if (config.assembly) {
69
- return [config.assembly.name];
70
- }
71
- return [];
29
+ // readConfigFile, not a bare readFileSync: run from the wrong directory, this
30
+ // is the error the user sees, and it should name the config.json it looked for
31
+ const config = await readConfigFile(configPath);
32
+ return { config, configPath, outLocation: path.dirname(configPath) };
72
33
  }
73
34
  export function getAssemblyNames(config, assemblies) {
74
35
  const asms = assemblies
75
36
  ? parseCommaSeparatedString(assemblies)
76
- : extractAssemblyNamesFromConfig(config);
37
+ : (config.assemblies?.map(a => a.name) ??
38
+ (config.assembly ? [config.assembly.name] : []));
77
39
  if (!asms.length) {
78
40
  throw new Error('No assemblies found');
79
41
  }
80
42
  return asms;
81
43
  }
82
- export function getTrackConfigs(config, trackIds, assemblyName, excludeTrackIds) {
83
- const { tracks } = config;
84
- if (!tracks) {
85
- return [];
44
+ export function formatDryRun(trackConfigs) {
45
+ return trackConfigs.map(t => `${t.trackId}\t${t.adapter?.type}`).join('\n');
46
+ }
47
+ // why a track was left out, or undefined if it is indexable
48
+ function skipReason(track, assemblyName, excludeSet) {
49
+ if (assemblyName && !track.assemblyNames.includes(assemblyName)) {
50
+ return `not in assembly '${assemblyName}'`;
86
51
  }
87
- const trackIdsToIndex = trackIds?.length
88
- ? trackIds
89
- : tracks.map(track => track.trackId);
90
- const excludeSet = new Set(excludeTrackIds || []);
91
- return trackIdsToIndex
92
- .map(trackId => {
93
- const currentTrack = tracks.find(t => trackId === t.trackId);
94
- if (!currentTrack) {
95
- throw new Error(`Track not found in config.json for trackId ${trackId}, please add track configuration before indexing.`);
96
- }
97
- return currentTrack;
98
- })
99
- .filter(track => {
100
- if (excludeSet.has(track.trackId)) {
101
- // console.log(`Skipping ${track.trackId}: excluded via --exclude-tracks`)
102
- return false;
103
- }
104
- if (!supported(track.adapter?.type)) {
105
- return false;
106
- }
107
- if (assemblyName && !track.assemblyNames.includes(assemblyName)) {
108
- console.log(`Skipping ${track.trackId}: not in assembly '${assemblyName}'`);
109
- return false;
52
+ else if (excludeSet.has(track.trackId)) {
53
+ return 'excluded with --excludeTracks';
54
+ }
55
+ else if (track.metadata?.skipTextIndex) {
56
+ return 'metadata.skipTextIndex is set';
57
+ }
58
+ else if (!supported(track.adapter?.type)) {
59
+ return `adapter type ${track.adapter?.type} is not indexable`;
60
+ }
61
+ else {
62
+ return undefined;
63
+ }
64
+ }
65
+ export function getTrackConfigs(config, trackIds, assemblyName, excludeTrackIds) {
66
+ const tracks = config.tracks ?? [];
67
+ // when specific trackIds are requested every one must exist; otherwise
68
+ // consider all tracks in the config
69
+ const requested = trackIds?.length
70
+ ? trackIds.map(trackId => {
71
+ const track = tracks.find(t => t.trackId === trackId);
72
+ if (track) {
73
+ return track;
74
+ }
75
+ else {
76
+ throw new Error(`Track not found in config.json for trackId ${trackId}, please add track configuration before indexing.`);
77
+ }
78
+ })
79
+ : tracks;
80
+ const excludeSet = new Set(excludeTrackIds);
81
+ return requested.filter(track => {
82
+ const reason = skipReason(track, assemblyName, excludeSet);
83
+ // only narrate skips for tracks the user named: sweeping a multi-assembly
84
+ // config otherwise prints a line per track per assembly
85
+ if (reason && trackIds?.length) {
86
+ console.log(`Skipping ${track.trackId}: ${reason}`);
110
87
  }
111
- return true;
88
+ return !reason;
112
89
  });
113
90
  }
114
91
  export { sanitizeForFilename as sanitizeNameForPath } from '@jbrowse/text-indexing-core';
@@ -1,22 +1,36 @@
1
- import path from 'path';
2
- import { ensureTrixDir, prepareIndexDriverFlags, sanitizeNameForPath, } from "./config-utils.js";
3
- import { indexDriver, prepareFileTrackConfigs } from "./indexing-utils.js";
4
- import { validateFileInput } from "./validators.js";
1
+ import path from 'node:path';
2
+ import { formatDryRun, prepareIndexDriverFlags, sanitizeNameForPath, } from './config-utils.js';
3
+ import { indexDriver, prepareFileTrackConfigs } from './indexing-utils.js';
4
+ import { validateFileInput } from './validators.js';
5
5
  export async function indexFileList(flags) {
6
- const { out, target, fileId, file, attributes, quiet, exclude, prefixSize } = flags;
6
+ const { out, target, fileId, file, attributes, quiet, exclude, include, prefixSize, dryrun, } = flags;
7
7
  validateFileInput(file);
8
- const outFlag = target || out || '.';
9
- ensureTrixDir(outFlag);
8
+ // --out/--target names either the install directory or its config.json, as
9
+ // every other command accepts; the trix output goes beside the config, and
10
+ // pointing at the config.json used to mkdir config.json/trix and fail ENOTDIR
11
+ const outArg = target || out || '.';
12
+ const outLocation = outArg.endsWith('.json') ? path.dirname(outArg) : outArg;
10
13
  const trackConfigs = prepareFileTrackConfigs(file, fileId);
11
- const name = trackConfigs.length > 1
12
- ? 'aggregate'
13
- : sanitizeNameForPath(path.basename(file[0]));
14
- await indexDriver({
15
- trackConfigs,
16
- outLocation: outFlag,
17
- name,
18
- assemblyNames: [],
19
- ...prepareIndexDriverFlags({ attributes, exclude, quiet, prefixSize }),
20
- });
21
- console.log('Successfully created index for these files. See https://jbrowse.org/storybook/lgv/main/?path=/story/text-searching--page for info about usage');
14
+ if (dryrun) {
15
+ console.log(formatDryRun(trackConfigs));
16
+ }
17
+ else {
18
+ const name = trackConfigs.length > 1
19
+ ? 'aggregate'
20
+ : sanitizeNameForPath(path.basename(file[0]));
21
+ await indexDriver({
22
+ trackConfigs,
23
+ outLocation,
24
+ name,
25
+ assemblyNames: [],
26
+ ...prepareIndexDriverFlags({
27
+ attributes,
28
+ exclude,
29
+ include,
30
+ quiet,
31
+ prefixSize,
32
+ }),
33
+ });
34
+ console.log('Successfully created index for these files. See https://jbrowse.org/storybook/lgv/with-aggregate-text-searching/ for info about usage');
35
+ }
22
36
  }
@@ -1,4 +1,4 @@
1
- export { aggregateIndex } from "./aggregate.js";
2
- export { perTrackIndex } from "./per-track.js";
3
- export { indexFileList } from "./file-list.js";
4
- export { run } from "./command.js";
1
+ export { aggregateIndex } from './aggregate.js';
2
+ export { perTrackIndex } from './per-track.js';
3
+ export { indexFileList } from './file-list.js';
4
+ export { run } from './command.js';
@@ -1,85 +1,56 @@
1
- import path from 'path';
2
- import { Readable } from 'stream';
3
- import { generateMeta, guessAdapterFromFileName, indexGff3, indexVcf, } from '@jbrowse/text-indexing-core';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Readable } from 'node:stream';
4
+ import { TRIX_DIR, generateMeta, guessAdapterFromFileName, indexFiles, trixFilePaths, } from '@jbrowse/text-indexing-core';
4
5
  import { Presets, SingleBar } from 'cli-progress';
5
6
  import { ixIxxStream } from 'ixixx';
6
- import { getAdapterLocation, getLoc } from "./adapter-utils.js";
7
- import { supported } from "../../types/common.js";
8
- export async function runIxIxx({ readStream, outLocation, name, prefixSize, quiet, }) {
7
+ import { supported } from '../../types/common.js';
8
+ // every index artifact lands in <outLocation>/trix, so the directory is created
9
+ // here rather than by each caller: --dryrun goes nowhere near indexDriver and so
10
+ // no longer leaves an empty trix/ behind
11
+ function ensureTrixDir(outLocation) {
12
+ fs.mkdirSync(path.join(outLocation, TRIX_DIR), { recursive: true });
13
+ }
14
+ async function runIxIxx({ readStream, outLocation, name, prefixSize, quiet, }) {
9
15
  const progressBar = new SingleBar({ format: '{bar} Sorting and writing index...', etaBuffer: 2000 }, Presets.shades_classic);
10
16
  if (!quiet) {
11
17
  progressBar.start(1, 0);
12
18
  }
13
- const ixPath = path.join(outLocation, 'trix', `${name}.ix`);
14
- const ixxPath = path.join(outLocation, 'trix', `${name}.ixx`);
15
- await ixIxxStream(readStream, ixPath, ixxPath, prefixSize);
19
+ const { ix, ixx } = trixFilePaths(outLocation, name);
20
+ await ixIxxStream(readStream, ix, ixx, prefixSize);
16
21
  if (!quiet) {
17
22
  progressBar.update(1);
18
23
  progressBar.stop();
19
24
  }
20
25
  }
21
- export async function* indexFiles({ trackConfigs, attributes, outLocation, quiet, typesToExclude, }) {
22
- for (const config of trackConfigs) {
23
- const { adapter, textSearching, trackId } = config;
24
- const { type } = adapter || {};
25
- const { indexingFeatureTypesToExclude = typesToExclude, indexingAttributes = attributes, } = textSearching || {};
26
- const loc = getAdapterLocation(adapter);
27
- if (!loc) {
28
- continue;
29
- }
30
- const progressBar = new SingleBar({
31
- format: `{bar} ${trackId} {percentage}% | ETA: {eta}s`,
32
- etaBuffer: 2000,
33
- }, Presets.shades_classic);
34
- if (type === 'Gff3TabixAdapter' || type === 'Gff3Adapter') {
35
- yield* indexGff3({
36
- config,
37
- attributesToIndex: indexingAttributes,
38
- inLocation: getLoc(loc),
39
- outDir: outLocation,
40
- featureTypesToExclude: indexingFeatureTypesToExclude,
41
- onStart: totalBytes => {
42
- if (!quiet) {
43
- progressBar.start(totalBytes, 0);
44
- }
45
- },
46
- onUpdate: receivedBytes => {
47
- if (!quiet) {
48
- progressBar.update(receivedBytes);
49
- }
50
- },
51
- });
52
- }
53
- else if (type === 'VcfTabixAdapter' || type === 'VcfAdapter') {
54
- yield* indexVcf({
55
- config,
56
- attributesToIndex: indexingAttributes,
57
- inLocation: getLoc(loc),
58
- outDir: outLocation,
59
- onStart: totalBytes => {
60
- if (!quiet) {
26
+ export async function indexDriver({ trackConfigs, attributes, outLocation, name, quiet, typesToExclude, typesToInclude, assemblyNames, prefixSize, }) {
27
+ ensureTrixDir(outLocation);
28
+ const readStream = Readable.from(indexFiles({
29
+ tracks: trackConfigs,
30
+ attributesToIndex: attributes,
31
+ outDir: outLocation,
32
+ featureTypesToExclude: typesToExclude,
33
+ featureTypesToInclude: typesToInclude,
34
+ makeProgress: quiet
35
+ ? undefined
36
+ : trackId => {
37
+ const progressBar = new SingleBar({
38
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {bar}/{percentage}/{eta} are cli-progress format tokens, not JS interpolation
39
+ format: `{bar} ${trackId} {percentage}% | ETA: {eta}s`,
40
+ etaBuffer: 2000,
41
+ }, Presets.shades_classic);
42
+ return {
43
+ onStart: totalBytes => {
61
44
  progressBar.start(totalBytes, 0);
62
- }
63
- },
64
- onUpdate: receivedBytes => {
65
- if (!quiet) {
45
+ },
46
+ onUpdate: receivedBytes => {
66
47
  progressBar.update(receivedBytes);
67
- }
68
- },
69
- });
70
- }
71
- if (!quiet) {
72
- progressBar.stop();
73
- }
74
- }
75
- }
76
- export async function indexDriver({ trackConfigs, attributes, outLocation, name, quiet, typesToExclude, assemblyNames, prefixSize, }) {
77
- const readStream = Readable.from(indexFiles({
78
- trackConfigs,
79
- attributes,
80
- outLocation,
81
- quiet,
82
- typesToExclude,
48
+ },
49
+ onDone: () => {
50
+ progressBar.stop();
51
+ },
52
+ };
53
+ },
83
54
  }));
84
55
  await runIxIxx({
85
56
  readStream,
@@ -94,17 +65,22 @@ export async function indexDriver({ trackConfigs, attributes, outLocation, name,
94
65
  outDir: outLocation,
95
66
  name,
96
67
  featureTypesToExclude: typesToExclude,
68
+ featureTypesToInclude: typesToInclude,
97
69
  assemblyNames,
98
70
  });
99
71
  }
100
72
  export function prepareFileTrackConfigs(files, fileIds) {
101
- return files
102
- .map((file, i) => {
73
+ return files.map((file, i) => {
103
74
  const config = guessAdapterFromFileName(file);
104
75
  if (fileIds?.[i]) {
105
76
  config.trackId = fileIds[i];
106
77
  }
78
+ // throw rather than silently filtering: a user who named the file with
79
+ // --file should be told it can't be indexed instead of getting a misleading
80
+ // "successfully created index" with no output
81
+ if (!supported(config.adapter?.type)) {
82
+ throw new Error(`Cannot text-index ${file}: adapter type ${config.adapter?.type} is not indexable (only GFF3, GTF and VCF are supported)`);
83
+ }
107
84
  return config;
108
- })
109
- .filter(fileConfig => supported(fileConfig.adapter?.type));
85
+ });
110
86
  }
@@ -1,18 +1,22 @@
1
- import { createTrixAdapter } from "./adapter-utils.js";
2
- import { getTrackConfigs, loadConfigForIndexing, parseCommaSeparatedString, prepareIndexDriverFlags, writeConf, } from "./config-utils.js";
3
- import { indexDriver } from "./indexing-utils.js";
4
- import { validateAssembliesForPerTrack } from "./validators.js";
5
- import { resolveConfigPath } from "../../utils.js";
1
+ import { parseCommaSeparatedString, writeJsonFile } from '../../utils.js';
2
+ import { createTrixAdapter } from './adapter-utils.js';
3
+ import { formatDryRun, getTrackConfigs, loadConfigForIndexing, prepareIndexDriverFlags, } from './config-utils.js';
4
+ import { indexDriver } from './indexing-utils.js';
5
+ import { validateAssembliesForPerTrack, validateTrackConfigs, } from './validators.js';
6
6
  export async function perTrackIndex(flags) {
7
- const { out, target, tracks, excludeTracks, assemblies, attributes, quiet, force, exclude, prefixSize, } = flags;
8
- const { config, configPath, outLocation } = await loadConfigForIndexing(target, out, resolveConfigPath);
9
- const configTracks = config.tracks || [];
7
+ const { out, target, tracks, excludeTracks, assemblies, attributes, quiet, force, exclude, include, prefixSize, dryrun, } = flags;
10
8
  validateAssembliesForPerTrack(assemblies);
9
+ const { config, configPath, outLocation } = await loadConfigForIndexing(target, out);
10
+ const configTracks = config.tracks ?? [];
11
11
  const confs = getTrackConfigs(config, parseCommaSeparatedString(tracks), undefined, parseCommaSeparatedString(excludeTracks));
12
- if (!confs.length) {
13
- throw new Error('Tracks not found in config.json, please add track configurations before indexing.');
12
+ validateTrackConfigs(confs);
13
+ if (dryrun) {
14
+ console.log(formatDryRun(confs));
15
+ return;
14
16
  }
15
- let hasChanges = false;
17
+ // sequential, not Promise.all: each track's indexer owns the progress bar and
18
+ // streams a whole file through ixIxx
19
+ const indexed = [];
16
20
  for (const trackConfig of confs) {
17
21
  const { textSearching, trackId, assemblyNames } = trackConfig;
18
22
  if (textSearching?.textSearchAdapter && !force) {
@@ -25,27 +29,36 @@ export async function perTrackIndex(flags) {
25
29
  outLocation,
26
30
  name: trackId,
27
31
  assemblyNames,
28
- ...prepareIndexDriverFlags({ attributes, exclude, quiet, prefixSize }),
32
+ ...prepareIndexDriverFlags({
33
+ attributes,
34
+ exclude,
35
+ include,
36
+ quiet,
37
+ prefixSize,
38
+ }),
29
39
  });
30
- if (!textSearching?.textSearchAdapter) {
31
- // modifies track with new text search adapter
32
- const index = configTracks.findIndex(track => trackId === track.trackId);
33
- if (index !== -1) {
34
- configTracks[index] = {
35
- ...trackConfig,
36
- textSearching: {
37
- ...textSearching,
38
- textSearchAdapter: createTrixAdapter(trackId, assemblyNames),
39
- },
40
- };
41
- hasChanges = true;
42
- }
43
- else {
44
- console.warn(`Warning: can't find trackId ${trackId}`);
45
- }
46
- }
40
+ indexed.push(trackConfig);
47
41
  }
48
- if (hasChanges) {
49
- writeConf({ ...config, tracks: configTracks }, configPath);
42
+ // a track that already carried an adapter keeps it — re-indexing under --force
43
+ // refreshes the trix files the existing adapter already points at. The rest
44
+ // gain one naming the files just written. getTrackConfigs returns elements of
45
+ // config.tracks, so every indexed track is somewhere in configTracks.
46
+ const newAdapters = new Map(indexed
47
+ .filter(track => !track.textSearching?.textSearchAdapter)
48
+ .map(track => [
49
+ track.trackId,
50
+ createTrixAdapter(track.trackId, track.assemblyNames),
51
+ ]));
52
+ const updatedTracks = configTracks.map(track => {
53
+ const textSearchAdapter = newAdapters.get(track.trackId);
54
+ return textSearchAdapter
55
+ ? {
56
+ ...track,
57
+ textSearching: { ...track.textSearching, textSearchAdapter },
58
+ }
59
+ : track;
60
+ });
61
+ if (updatedTracks.some((track, i) => track !== configTracks[i])) {
62
+ await writeJsonFile(configPath, { ...config, tracks: updatedTracks });
50
63
  }
51
64
  }
@@ -4,7 +4,7 @@ export function validateTrackConfigs(tracks) {
4
4
  }
5
5
  }
6
6
  export function validateFileInput(file) {
7
- if (!file) {
7
+ if (!file?.length) {
8
8
  throw new Error('Cannot index file list without files');
9
9
  }
10
10
  }
@@ -1,9 +1,7 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { parseArgs } from 'util';
4
- import decompress from 'decompress';
5
- import fetch from "../cliFetch.js";
6
- import { fetchGithubVersions, getBranch, getLatest, getTag, printHelp, } from "../utils.js";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { downloadRelease, extractZip, printHelp, printVersions, } from '../utils.js';
7
5
  const description = 'Upgrades JBrowse 2 to latest version';
8
6
  const examples = [
9
7
  '# Upgrades current directory to latest jbrowse release',
@@ -65,8 +63,6 @@ export async function run(args) {
65
63
  args,
66
64
  allowPositionals: true,
67
65
  });
68
- const argsPath = positionals[0];
69
- const { clean, listVersions, tag, url, branch, nightly } = runFlags;
70
66
  if (runFlags.help) {
71
67
  printHelp({
72
68
  options,
@@ -76,41 +72,33 @@ export async function run(args) {
76
72
  });
77
73
  return;
78
74
  }
75
+ const { clean, listVersions, tag, url, branch, nightly } = runFlags;
79
76
  if (listVersions) {
80
- const versions = (await fetchGithubVersions()).map(v => v.tag_name);
81
- console.log(`All JBrowse versions:\n${versions.join('\n')}`);
82
- process.exit(0);
83
- }
84
- if (!argsPath) {
85
- throw new Error('No directory supplied');
77
+ await printVersions();
78
+ return;
86
79
  }
80
+ // localPath is optional: `jbrowse upgrade` upgrades the install in the
81
+ // current directory, as the examples and docs advertise
82
+ const argsPath = positionals[0] ?? '.';
87
83
  if (!fs.existsSync(path.join(argsPath, 'manifest.json'))) {
88
84
  throw new Error(`No manifest.json found in this directory, are you sure it is an
89
85
  existing jbrowse 2 installation?`);
90
86
  }
91
- const locationUrl = url ||
92
- (nightly ? await getBranch('main') : '') ||
93
- (branch ? await getBranch(branch) : '') ||
94
- (tag ? await getTag(tag) : await getLatest());
95
- console.log(`Fetching ${locationUrl}...`);
96
- const response = await fetch(locationUrl);
97
- if (!response.ok) {
98
- throw new Error(`HTTP ${response.status} fetching ${locationUrl}: ${response.statusText}`);
99
- }
100
- const type = response.headers.get('content-type');
101
- if (url &&
102
- type !== 'application/zip' &&
103
- type !== 'application/octet-stream') {
104
- throw new Error('The URL provided does not seem to be a JBrowse installation URL');
105
- }
87
+ const { locationUrl, archive } = await downloadRelease({
88
+ url,
89
+ nightly,
90
+ branch,
91
+ tag,
92
+ });
106
93
  if (clean) {
107
94
  fs.rmSync(path.join(argsPath, 'static'), { recursive: true, force: true });
108
- for (const f of fs
95
+ const workerFiles = fs
109
96
  .readdirSync(argsPath)
110
- .filter(f => f.includes('worker.js'))) {
97
+ .filter(f => f.includes('worker.js'));
98
+ for (const f of workerFiles) {
111
99
  fs.unlinkSync(path.join(argsPath, f));
112
100
  }
113
101
  }
114
- await decompress(Buffer.from(await response.arrayBuffer()), argsPath);
102
+ await extractZip(archive, argsPath);
115
103
  console.log(`Unpacked ${locationUrl} at ${argsPath}`);
116
104
  }