@jbrowse/cli 4.3.0 → 5.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +451 -72
- package/dist/base.js +0 -3
- package/dist/bin.js +2 -1
- package/dist/cliFetch.js +3 -0
- package/dist/commands/add-assembly/index.js +46 -19
- package/dist/commands/add-assembly/utils.js +204 -290
- package/dist/commands/add-connection.js +73 -79
- package/dist/commands/add-track-json.js +24 -12
- package/dist/commands/add-track-utils/adapter-utils.js +189 -288
- package/dist/commands/add-track-utils/file-operations.js +27 -11
- package/dist/commands/add-track-utils/multiwig.js +47 -0
- package/dist/commands/add-track-utils/track-config.js +66 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +200 -62
- package/dist/commands/admin-server/index.js +39 -50
- package/dist/commands/admin-server/utils.js +238 -126
- package/dist/commands/create.js +18 -34
- package/dist/commands/make-density/density-generator.js +180 -0
- package/dist/commands/make-density/index.js +139 -0
- package/dist/commands/make-pif/index.js +133 -11
- package/dist/commands/make-pif/pif-generator.js +243 -43
- package/dist/commands/process-utils.js +13 -2
- package/dist/commands/remove-track.js +20 -13
- package/dist/commands/set-default-session.js +35 -31
- package/dist/commands/shared/config-operations.js +3 -10
- package/dist/commands/shared/density.js +12 -0
- package/dist/commands/shared/sort-utils.js +67 -10
- package/dist/commands/shared/validators.js +38 -3
- package/dist/commands/sort-bed.js +2 -33
- package/dist/commands/sort-gff.js +2 -33
- package/dist/commands/text-index/adapter-utils.js +19 -26
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +37 -7
- package/dist/commands/text-index/config-utils.js +62 -85
- package/dist/commands/text-index/file-list.js +32 -18
- package/dist/commands/text-index/index.js +4 -4
- package/dist/commands/text-index/indexing-utils.js +44 -96
- package/dist/commands/text-index/per-track.js +44 -31
- package/dist/commands/text-index/validators.js +1 -1
- package/dist/commands/upgrade.js +20 -32
- package/dist/commands/validate/configManifest.generated.js +7613 -0
- package/dist/commands/validate/configSchema.generated.js +20400 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +113 -0
- package/dist/commands/validate/liftConfig.js +34 -0
- package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
- package/dist/commands/validate/markRules/autoBin.js +27 -0
- package/dist/commands/validate/markRules/colorScale.js +244 -0
- package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
- package/dist/commands/validate/markRules/jexlStrings.js +23 -0
- package/dist/commands/validate/markRules/markProblems.js +532 -0
- package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
- package/dist/commands/validate/markRules/markSpecs.js +96 -0
- package/dist/commands/validate/markRules/markVocabulary.js +57 -0
- package/dist/commands/validate/markRules/shapeNameList.js +7 -0
- package/dist/commands/validate/markRules/stepChannels.js +55 -0
- package/dist/commands/validate/schemaValidate.js +425 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +502 -0
- package/dist/index.js +104 -50
- package/dist/types/common.js +3 -27
- package/dist/utils.js +309 -128
- package/dist/version.js +1 -1
- package/package.json +8 -14
- package/bin/run +0 -4
- package/bundle/index.js +0 -3686
- package/dist/commands/make-pif/cigar-utils.js +0 -24
- package/dist/commands/make-pif/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -1,36 +1,25 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
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 (!
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
+
chunks.push(buffer);
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
app.post('/updateConfig', (req, res) => {
|
|
89
|
-
const { body } = req;
|
|
145
|
+
if (validation.ok) {
|
|
90
146
|
const config = body.config;
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
133
|
-
res.send('Error: Failed to read config');
|
|
183
|
+
sendText(res, 500, 'Error: Failed to read config');
|
|
134
184
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
*
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
}
|
package/dist/commands/create.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { parseArgs } from 'util';
|
|
3
|
-
import
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
81
|
+
await mkdir(argsPath, { recursive: true });
|
|
89
82
|
if (!force) {
|
|
90
83
|
await checkPath(argsPath);
|
|
91
84
|
}
|
|
92
|
-
const locationUrl =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
}
|