@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,36 +1,25 @@
1
- import crypto from 'crypto';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { debug, resolveConfigPath, writeJsonFile } from "../../utils.js";
5
- import { createDefaultConfig } from "../add-assembly/utils.js";
6
- /**
7
- * Validates if a port number is in the valid range
8
- */
9
- export function isValidPort(port) {
10
- return port > 0 && port < 65535;
11
- }
12
- /**
13
- * Parses and validates a port string
14
- */
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import handler from 'serve-handler';
5
+ import { debug, isRecord, resolveConfigPath, writeJsonFile, } from '../../utils.js';
6
+ import { createDefaultConfig } from '../add-assembly/utils.js';
7
+ // Loopback, so the unauthenticated static handler below does not serve the
8
+ // data directory to every host on the network. `--host` opts out.
9
+ export const DEFAULT_HOST = '127.0.0.1';
15
10
  export function parsePort({ portStr, defaultPort = 9090, }) {
16
11
  if (!portStr) {
17
12
  return defaultPort;
18
13
  }
19
14
  const parsedPort = Number.parseInt(portStr, 10);
20
- if (!isValidPort(parsedPort)) {
15
+ if (!(parsedPort > 0 && parsedPort <= 65535)) {
21
16
  throw new Error(`${portStr} is not a valid port`);
22
17
  }
23
18
  return parsedPort;
24
19
  }
25
- /**
26
- * Generates a random alphanumeric string to serve as admin key
27
- */
28
20
  export function generateKey() {
29
- return crypto.randomBytes(5).toString('hex');
21
+ return crypto.randomBytes(16).toString('hex');
30
22
  }
31
- /**
32
- * Sets up the configuration file
33
- */
34
23
  export async function setupConfigFile({ root = '.', } = {}) {
35
24
  const outFile = await resolveConfigPath(root);
36
25
  const baseDir = path.dirname(outFile);
@@ -43,131 +32,264 @@ export async function setupConfigFile({ root = '.', } = {}) {
43
32
  }
44
33
  return { outFile, baseDir };
45
34
  }
35
+ const BYTE_UNITS = {
36
+ b: 1,
37
+ kb: 1024,
38
+ mb: 1024 ** 2,
39
+ gb: 1024 ** 3,
40
+ };
46
41
  /**
47
- * Validates admin key and extracts config path from request
42
+ * `--bodySizeLimit` as a byte count. Replaces the `bytes` package that
43
+ * `express.json` used, and covers the suffixes a config update could plausibly
44
+ * be given; anything else is a mistake at the command line and says so, rather
45
+ * than silently becoming a default the user did not ask for.
48
46
  */
49
- function validateAndExtractParams({ req, key, baseDir, outFile, }) {
50
- const { body } = req;
51
- // Check for adminKey in both query parameters and request body for backward compatibility
52
- const adminKeyFromQuery = req.query.adminKey;
53
- const adminKeyFromBody = body?.adminKey;
54
- const adminKey = adminKeyFromBody || adminKeyFromQuery;
55
- if (adminKey !== key) {
56
- return { isValid: false, error: 'Invalid admin key' };
47
+ export function parseByteLimit(limit) {
48
+ const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i.exec(limit.trim());
49
+ const amount = match?.[1];
50
+ const scale = BYTE_UNITS[(match?.[2] ?? 'b').toLowerCase()];
51
+ if (amount === undefined || scale === undefined) {
52
+ throw new Error(`${limit} is not a valid body size limit; use a number optionally followed by b, kb, mb or gb`);
57
53
  }
58
- // Get configPath from either body or query parameters
59
- const configPathParam = body?.configPath || req.query.config;
60
- try {
61
- // Normalize the config path
62
- const configPath = configPathParam
63
- ? path.normalize(path.join(baseDir, configPathParam))
64
- : outFile;
65
- // Check for directory traversal attempts
66
- const normalizedBaseDir = path.normalize(baseDir);
67
- const relPath = path.relative(normalizedBaseDir, configPath);
68
- // Ensure the config path is within the base directory and doesn't contain path traversal
69
- if (relPath.startsWith('..') || path.isAbsolute(relPath)) {
70
- return { isValid: false, error: 'Cannot perform directory traversal' };
54
+ return Math.floor(Number(amount) * scale);
55
+ }
56
+ /** A request body that was over the limit, so the handler can answer 413. */
57
+ class PayloadTooLargeError extends Error {
58
+ }
59
+ /**
60
+ * The JSON body, or `undefined` when the request carried none. The size is
61
+ * counted as it streams rather than after, so an oversized upload is refused
62
+ * without being buffered first — which is the only reason the limit exists.
63
+ */
64
+ async function readJsonBody(req, limitBytes) {
65
+ const chunks = [];
66
+ let size = 0;
67
+ for await (const chunk of req) {
68
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
69
+ size += buffer.length;
70
+ if (size > limitBytes) {
71
+ throw new PayloadTooLargeError(`request body exceeds ${limitBytes} bytes`);
71
72
  }
72
- return { isValid: true, configPath };
73
+ chunks.push(buffer);
73
74
  }
74
- catch (error) {
75
- return { isValid: false, error: 'Failed to validate config path' };
75
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
76
+ const parsed = raw === '' ? undefined : JSON.parse(raw);
77
+ return parsed;
78
+ }
79
+ // A missing or non-object body yields no properties instead of throwing, so a
80
+ // route reading it before checking the key cannot turn an unauthorized request
81
+ // into a 500.
82
+ function requestBody(body) {
83
+ return isRecord(body) ? body : {};
84
+ }
85
+ // body and query values are untrusted input: a repeated query param arrives as
86
+ // an array, a nested one as an object, and neither is a key or a path
87
+ function asString(value) {
88
+ return typeof value === 'string' ? value : undefined;
89
+ }
90
+ // The shape `express` gave `req.query`, which the checks below are written
91
+ // against: absent, one value, or an array when the param was repeated. An array
92
+ // is not a string, so `asString` rejects it and a repeated `adminKey` or
93
+ // `config` fails rather than quietly using whichever copy came first.
94
+ function queryValue(params, name) {
95
+ const values = params.getAll(name);
96
+ return values.length === 0
97
+ ? undefined
98
+ : values.length === 1
99
+ ? values[0]
100
+ : values;
101
+ }
102
+ function validateAndExtractParams({ body, query, key, baseDir, outFile, }) {
103
+ const adminKey = asString(body.adminKey) || asString(queryValue(query, 'adminKey'));
104
+ if (adminKey !== key) {
105
+ return { ok: false, error: 'Invalid admin key' };
106
+ }
107
+ // a configPath that is there but is not a string (a JSON number, an object, a
108
+ // repeated query param) is a malformed request, not an absent one — it must not
109
+ // silently fall through to writing the default config
110
+ const rawConfigPath = body.configPath || queryValue(query, 'config');
111
+ const configPathParam = asString(rawConfigPath);
112
+ if (rawConfigPath !== undefined && configPathParam === undefined) {
113
+ return { ok: false, error: 'Failed to validate config path' };
114
+ }
115
+ const configPath = configPathParam
116
+ ? path.normalize(path.join(baseDir, configPathParam))
117
+ : outFile;
118
+ const relPath = path.relative(path.normalize(baseDir), configPath);
119
+ if (relPath.startsWith('..') || path.isAbsolute(relPath)) {
120
+ return { ok: false, error: 'Cannot perform directory traversal' };
76
121
  }
122
+ return { ok: true, configPath };
77
123
  }
78
- /**
79
- * Sets up the API routes for the server
80
- */
81
- export function setupRoutes({ app, baseDir, outFile, key, serverRef, }) {
82
- // Root route
83
- app.get('/', (_req, res) => {
84
- res.setHeader('Content-Type', 'text/plain');
85
- res.send('JBrowse Admin Server');
124
+ function sendText(res, status, body) {
125
+ res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
126
+ res.end(body);
127
+ }
128
+ // What `cors()` sent. The admin key is what authorizes a write; the origin
129
+ // never was.
130
+ function applyCorsHeaders(res) {
131
+ res.setHeader('Access-Control-Allow-Origin', '*');
132
+ res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, POST, OPTIONS');
133
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
134
+ }
135
+ function updateConfig({ res, body, query, key, baseDir, outFile, }) {
136
+ // the key is checked before the payload is touched, so a request with no JSON
137
+ // body gets the 401 it earned rather than a 500 from reading it
138
+ const validation = validateAndExtractParams({
139
+ body,
140
+ query,
141
+ key,
142
+ baseDir,
143
+ outFile,
86
144
  });
87
- // Update config route
88
- app.post('/updateConfig', (req, res) => {
89
- const { body } = req;
145
+ if (validation.ok) {
90
146
  const config = body.config;
91
- const validation = validateAndExtractParams({ req, key, baseDir, outFile });
92
- if (!validation.isValid) {
93
- res.status(401).setHeader('Content-Type', 'text/plain');
94
- res.send(`Error: ${validation.error}`);
95
- return;
147
+ if (config === undefined) {
148
+ sendText(res, 400, 'Error: Missing config in request body');
96
149
  }
97
- // Remove adminKey from body before saving to config
98
- if (body.adminKey) {
99
- delete body.adminKey;
100
- }
101
- try {
102
- fs.writeFileSync(validation.configPath, JSON.stringify(config, null, 2));
103
- res.setHeader('Content-Type', 'text/plain');
104
- res.send('Config updated successfully');
105
- }
106
- catch (error) {
107
- res.status(500).setHeader('Content-Type', 'text/plain');
108
- res.send('Error: Failed to update config');
150
+ else {
151
+ try {
152
+ fs.writeFileSync(validation.configPath, JSON.stringify(config, null, 2));
153
+ sendText(res, 200, 'Config updated successfully');
154
+ }
155
+ catch {
156
+ sendText(res, 500, 'Error: Failed to update config');
157
+ }
109
158
  }
159
+ }
160
+ else {
161
+ sendText(res, 401, `Error: ${validation.error}`);
162
+ }
163
+ }
164
+ function readConfig({ res, body, query, key, baseDir, outFile, }) {
165
+ const validation = validateAndExtractParams({
166
+ body,
167
+ query,
168
+ key,
169
+ baseDir,
170
+ outFile,
110
171
  });
111
- // Get config route
112
- app.get('/config', (req, res) => {
113
- const validation = validateAndExtractParams({ req, key, baseDir, outFile });
114
- if (!validation.isValid) {
115
- res.status(401).setHeader('Content-Type', 'text/plain');
116
- res.send(`Error: ${validation.error}`);
117
- return;
118
- }
172
+ if (validation.ok) {
119
173
  try {
120
174
  if (fs.existsSync(validation.configPath)) {
121
- const config = fs.readFileSync(validation.configPath, 'utf8');
122
- res.setHeader('Content-Type', 'text/plain');
123
- res.send(config);
175
+ sendText(res, 200, fs.readFileSync(validation.configPath, 'utf8'));
124
176
  }
125
177
  else {
126
- res.status(404).setHeader('Content-Type', 'text/plain');
127
- res.send('Error: Config file not found');
178
+ sendText(res, 404, 'Error: Config file not found');
128
179
  }
129
180
  }
130
181
  catch (error) {
131
182
  console.error('Error reading config:', error);
132
- res.status(500).setHeader('Content-Type', 'text/plain');
133
- res.send('Error: Failed to read config');
183
+ sendText(res, 500, 'Error: Failed to read config');
134
184
  }
135
- });
136
- // Shutdown route for testing
137
- app.post('/shutdown', (req, res) => {
138
- const { body } = req;
139
- const adminKey = body?.adminKey;
140
- if (adminKey !== key) {
141
- res.status(401).setHeader('Content-Type', 'text/plain');
142
- res.send('Error: Invalid admin key');
143
- return;
144
- }
145
- res.setHeader('Content-Type', 'text/plain');
146
- res.send('Server shutting down');
147
- // Shutdown the server after sending response
148
- setImmediate(() => {
149
- if (serverRef.current) {
150
- serverRef.current.close();
151
- }
152
- });
153
- });
185
+ }
186
+ else {
187
+ sendText(res, 401, `Error: ${validation.error}`);
188
+ }
189
+ }
190
+ /**
191
+ * The static half, and why it runs for `/` before the greeting below does: an
192
+ * admin server is pointed at a JBrowse install, so `/` has to load the app the
193
+ * user came for. `serve-handler` owns the parts worth not writing — content
194
+ * types, byte ranges (the same directory holds the BAM and CRAM files the app
195
+ * fetches by range), directory handling and the traversal guard.
196
+ */
197
+ async function serveStatic({ req, res, baseDir, }) {
198
+ await handler(req, res, { public: baseDir });
199
+ }
200
+ async function hasIndexHtml(baseDir) {
201
+ return fs.promises
202
+ .stat(path.join(baseDir, 'index.html'))
203
+ .then(stats => stats.isFile())
204
+ .catch(() => false);
154
205
  }
155
206
  /**
156
- * Starts the server and sets up shutdown handlers
207
+ * The whole server, as one request handler.
208
+ *
209
+ * Replaces express + cors + express.json. The four routes are exact paths with
210
+ * no parameters, so the dispatch is a switch; the rest of what express supplied
211
+ * is the body reader, the CORS headers and the static handler above.
212
+ *
213
+ * One deliberate difference: `GET /config` is always the admin route. Under
214
+ * `express.static` a file literally named `config` sitting in the JBrowse
215
+ * directory would have shadowed it, which is not a thing anyone wants and was
216
+ * never intended.
157
217
  */
158
- export function startServer({ app, port, key, outFile, keyPath, serverRef, }) {
159
- // Start the server
160
- const server = app.listen(port, () => {
161
- console.log(`Admin server started on port ${port}\n\n` +
218
+ export function createRequestHandler({ baseDir, outFile, key, serverRef, bodySizeLimit, }) {
219
+ return async (req, res) => {
220
+ try {
221
+ applyCorsHeaders(res);
222
+ const url = new URL(req.url ?? '/', 'http://localhost');
223
+ const route = `${req.method} ${url.pathname}`;
224
+ if (req.method === 'OPTIONS') {
225
+ res.writeHead(204);
226
+ res.end();
227
+ }
228
+ else if (route === 'POST /updateConfig' || route === 'GET /config') {
229
+ const body = requestBody(await readJsonBody(req, bodySizeLimit));
230
+ const args = {
231
+ res,
232
+ body,
233
+ query: url.searchParams,
234
+ key,
235
+ baseDir,
236
+ outFile,
237
+ };
238
+ if (route === 'GET /config') {
239
+ readConfig(args);
240
+ }
241
+ else {
242
+ updateConfig(args);
243
+ }
244
+ }
245
+ else if (route === 'POST /shutdown') {
246
+ const body = requestBody(await readJsonBody(req, bodySizeLimit));
247
+ if (asString(body.adminKey) === key) {
248
+ sendText(res, 200, 'Server shutting down');
249
+ setImmediate(() => {
250
+ serverRef.current?.close();
251
+ });
252
+ }
253
+ else {
254
+ sendText(res, 401, 'Error: Invalid admin key');
255
+ }
256
+ }
257
+ else if (url.pathname === '/' && !(await hasIndexHtml(baseDir))) {
258
+ sendText(res, 200, 'JBrowse Admin Server');
259
+ }
260
+ else {
261
+ await serveStatic({ req, res, baseDir });
262
+ }
263
+ }
264
+ catch (error) {
265
+ // the express error middleware this replaces, and registered after the
266
+ // routes for the same reason: it has to catch what they and the body
267
+ // reader throw
268
+ if (!res.headersSent) {
269
+ if (error instanceof PayloadTooLargeError) {
270
+ sendText(res, 413, 'Error: Request body too large');
271
+ }
272
+ else if (error instanceof SyntaxError) {
273
+ sendText(res, 400, 'Error: Malformed JSON in request body');
274
+ }
275
+ else {
276
+ console.error('Server error:', error);
277
+ sendText(res, 500, 'Internal Server Error');
278
+ }
279
+ }
280
+ }
281
+ };
282
+ }
283
+ export function startServer({ server, port, host = DEFAULT_HOST, key, outFile, serverRef, }) {
284
+ server.listen(port, host, () => {
285
+ console.log(`Admin server started on ${host}:${port}\n\n` +
162
286
  `To access the admin interface, open your browser to:\n` +
163
287
  `http://localhost:${port}?adminKey=${key}\n\n` +
164
288
  `Admin key: ${key}\n` +
165
289
  `Config file: ${outFile}\n\n` +
166
290
  `To stop the server, press Ctrl+C`);
167
291
  });
168
- // Store server reference for shutdown route
169
292
  serverRef.current = server;
170
- // Handle server errors
171
293
  server.on('error', (error) => {
172
294
  if (error.code === 'EADDRINUSE') {
173
295
  console.error(`Error: Port ${port} is already in use`);
@@ -177,22 +299,12 @@ export function startServer({ app, port, key, outFile, keyPath, serverRef, }) {
177
299
  }
178
300
  process.exit(1);
179
301
  });
180
- // Common shutdown handler
181
302
  const shutdownHandler = () => {
182
303
  console.log('\nShutting down admin server...');
183
304
  server.close(() => {
184
- // Clean up admin key file
185
- try {
186
- fs.unlinkSync(keyPath);
187
- debug(`Removed admin key file: ${keyPath}`);
188
- }
189
- catch (error) {
190
- // Ignore errors when cleaning up
191
- }
192
305
  process.exit(0);
193
306
  });
194
307
  };
195
- // Handle server shutdown
196
308
  process.on('SIGINT', shutdownHandler);
197
309
  process.on('SIGTERM', shutdownHandler);
198
310
  }
@@ -1,9 +1,6 @@
1
- import fs from 'fs';
2
- import { parseArgs } from 'util';
3
- import decompress from 'decompress';
4
- import fetch from "../cliFetch.js";
5
- import { fetchGithubVersions, getBranch, getLatest, getTag, printHelp, } from "../utils.js";
6
- const fsPromises = fs.promises;
1
+ import { mkdir, readdir } from 'node:fs/promises';
2
+ import { parseArgs } from 'node:util';
3
+ import { downloadRelease, extractZip, printHelp, printVersions, } from '../utils.js';
7
4
  const description = 'Downloads and installs the latest JBrowse 2 release';
8
5
  const examples = [
9
6
  '# Download latest release from github, and put in specific path',
@@ -71,44 +68,31 @@ export async function run(args) {
71
68
  });
72
69
  return;
73
70
  }
74
- const argsPath = positionals[0];
75
- if (!argsPath && !runFlags.listVersions) {
76
- console.error(`Missing 1 required arg:
77
- localPath Location where JBrowse 2 will be installed
78
- See more help with --help`);
79
- return;
80
- }
81
71
  const { force, url, listVersions, tag, branch, nightly } = runFlags;
82
72
  if (listVersions) {
83
- const versions = (await fetchGithubVersions()).map(version => version.tag_name);
84
- console.log(`All JBrowse versions:\n${versions.join('\n')}`);
85
- process.exit(0);
73
+ await printVersions();
74
+ return;
75
+ }
76
+ const argsPath = positionals[0];
77
+ if (!argsPath) {
78
+ throw new Error('Missing 1 required arg:\nlocalPath Location where JBrowse 2 will be installed\nSee more help with --help');
86
79
  }
87
80
  // mkdir will do nothing if dir exists
88
- await fsPromises.mkdir(argsPath, { recursive: true });
81
+ await mkdir(argsPath, { recursive: true });
89
82
  if (!force) {
90
83
  await checkPath(argsPath);
91
84
  }
92
- const locationUrl = url ||
93
- (nightly ? await getBranch('main') : '') ||
94
- (branch ? await getBranch(branch) : '') ||
95
- (tag ? await getTag(tag) : await getLatest());
96
- console.log(`Fetching ${locationUrl}...`);
97
- const response = await fetch(locationUrl);
98
- if (!response.ok) {
99
- throw new Error(`HTTP ${response.status} fetching ${locationUrl}: ${response.statusText}`);
100
- }
101
- const type = response.headers.get('content-type');
102
- if (url &&
103
- type !== 'application/zip' &&
104
- type !== 'application/octet-stream') {
105
- throw new Error('The URL provided does not seem to be a JBrowse installation URL');
106
- }
107
- await decompress(Buffer.from(await response.arrayBuffer()), argsPath);
85
+ const { locationUrl, archive } = await downloadRelease({
86
+ url,
87
+ nightly,
88
+ branch,
89
+ tag,
90
+ });
91
+ await extractZip(archive, argsPath);
108
92
  console.log(`Unpacked ${locationUrl} at ${argsPath}`);
109
93
  }
110
94
  async function checkPath(userPath) {
111
- const allFiles = await fsPromises.readdir(userPath);
95
+ const allFiles = await readdir(userPath);
112
96
  if (allFiles.length > 0) {
113
97
  throw new Error(`This directory (${userPath}) has existing files and could cause conflicts with create. Please choose another directory or use the force flag to overwrite existing files`);
114
98
  }