@jbrowse/cli 3.6.0 → 3.6.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.
- package/bin/run +1 -1
- package/dist/base.js +5 -0
- package/dist/bin.js +3 -0
- package/dist/commands/add-assembly.js +143 -0
- package/dist/commands/add-connection.js +177 -0
- package/dist/commands/add-track-json.js +81 -0
- package/dist/commands/add-track-utils/adapter-utils.js +304 -0
- package/dist/commands/add-track-utils/file-operations.js +36 -0
- package/dist/commands/add-track-utils/track-config.js +63 -0
- package/dist/commands/add-track-utils/validators.js +74 -0
- package/dist/commands/add-track.js +193 -0
- package/dist/commands/admin-server-utils.js +238 -0
- package/dist/commands/admin-server.js +51 -0
- package/dist/commands/assembly-utils.js +410 -0
- package/dist/commands/create.js +121 -0
- package/dist/commands/make-pif-utils/cigar-utils.js +29 -0
- package/dist/commands/make-pif-utils/file-utils.js +38 -0
- package/dist/commands/make-pif-utils/pif-generator.js +64 -0
- package/dist/commands/make-pif-utils/validators.js +22 -0
- package/dist/commands/make-pif.js +58 -0
- package/dist/commands/remove-track.js +58 -0
- package/dist/commands/set-default-session.js +104 -0
- package/dist/commands/sort-bed-utils/constants.js +12 -0
- package/dist/commands/sort-bed-utils/process-utils.js +23 -0
- package/dist/commands/sort-bed-utils/sort-utils.js +24 -0
- package/dist/commands/sort-bed-utils/validators.js +22 -0
- package/dist/commands/sort-bed.js +49 -0
- package/dist/commands/sort-gff-utils/constants.js +13 -0
- package/dist/commands/sort-gff-utils/process-utils.js +23 -0
- package/dist/commands/sort-gff-utils/sort-utils.js +55 -0
- package/dist/commands/sort-gff-utils/validators.js +21 -0
- package/dist/commands/sort-gff.js +49 -0
- package/dist/commands/text-index-utils/adapter-utils.js +63 -0
- package/dist/commands/text-index-utils/aggregate.js +87 -0
- package/dist/commands/text-index-utils/config-utils.js +59 -0
- package/dist/commands/text-index-utils/file-list.js +31 -0
- package/dist/commands/text-index-utils/index.js +9 -0
- package/dist/commands/text-index-utils/indexing-utils.js +84 -0
- package/dist/commands/text-index-utils/per-track.js +65 -0
- package/dist/commands/text-index-utils/validators.js +20 -0
- package/dist/commands/text-index.js +113 -0
- package/dist/commands/track-utils.js +85 -0
- package/dist/commands/upgrade.js +122 -0
- package/dist/fetchWithProxy.js +12 -0
- package/dist/index.js +119 -0
- package/dist/types/common.js +128 -0
- package/dist/types/gff3Adapter.js +73 -0
- package/dist/types/vcfAdapter.js +76 -0
- package/dist/util.js +35 -0
- package/dist/utils.js +154 -0
- package/package.json +3 -3
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isValidPort = isValidPort;
|
|
7
|
+
exports.parsePort = parsePort;
|
|
8
|
+
exports.generateKey = generateKey;
|
|
9
|
+
exports.setupConfigFile = setupConfigFile;
|
|
10
|
+
exports.setupRoutes = setupRoutes;
|
|
11
|
+
exports.setupServer = setupServer;
|
|
12
|
+
exports.startServer = startServer;
|
|
13
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const cors_1 = __importDefault(require("cors"));
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const utils_1 = require("../utils");
|
|
20
|
+
/**
|
|
21
|
+
* Validates if a port number is in the valid range
|
|
22
|
+
*/
|
|
23
|
+
function isValidPort(port) {
|
|
24
|
+
return port > 0 && port < 65535;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parses and validates a port string
|
|
28
|
+
*/
|
|
29
|
+
function parsePort({ portStr, defaultPort = 9090, }) {
|
|
30
|
+
if (!portStr) {
|
|
31
|
+
return defaultPort;
|
|
32
|
+
}
|
|
33
|
+
const parsedPort = Number.parseInt(portStr, 10);
|
|
34
|
+
if (!isValidPort(parsedPort)) {
|
|
35
|
+
throw new Error(`${portStr} is not a valid port`);
|
|
36
|
+
}
|
|
37
|
+
return parsedPort;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generates a random alphanumeric string to serve as admin key
|
|
41
|
+
*/
|
|
42
|
+
function generateKey() {
|
|
43
|
+
return crypto_1.default.randomBytes(5).toString('hex');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sets up the configuration file
|
|
47
|
+
*/
|
|
48
|
+
async function setupConfigFile({ root = '.', } = {}) {
|
|
49
|
+
const output = root;
|
|
50
|
+
const isDir = fs_1.default.lstatSync(output).isDirectory();
|
|
51
|
+
const outFile = isDir ? `${output}/config.json` : output;
|
|
52
|
+
const baseDir = path_1.default.dirname(outFile);
|
|
53
|
+
if (fs_1.default.existsSync(outFile)) {
|
|
54
|
+
(0, utils_1.debug)(`Found existing config file ${outFile}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
(0, utils_1.debug)(`Creating config file ${outFile}`);
|
|
58
|
+
await (0, utils_1.writeJsonFile)(outFile, {
|
|
59
|
+
assemblies: [],
|
|
60
|
+
configuration: {},
|
|
61
|
+
connections: [],
|
|
62
|
+
defaultSession: {
|
|
63
|
+
name: 'New Session',
|
|
64
|
+
},
|
|
65
|
+
tracks: [],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return { outFile, baseDir };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validates admin key and extracts config path from request
|
|
72
|
+
*/
|
|
73
|
+
function validateAndExtractParams({ req, key, baseDir, outFile, }) {
|
|
74
|
+
const { body } = req;
|
|
75
|
+
// Check for adminKey in both query parameters and request body for backward compatibility
|
|
76
|
+
const adminKeyFromQuery = req.query.adminKey;
|
|
77
|
+
const adminKeyFromBody = body?.adminKey;
|
|
78
|
+
const adminKey = adminKeyFromBody || adminKeyFromQuery;
|
|
79
|
+
if (adminKey !== key) {
|
|
80
|
+
return { isValid: false, error: 'Invalid admin key' };
|
|
81
|
+
}
|
|
82
|
+
// Get configPath from either body or query parameters
|
|
83
|
+
const configPathParam = body?.configPath || req.query.config;
|
|
84
|
+
try {
|
|
85
|
+
// Normalize the config path
|
|
86
|
+
const configPath = configPathParam
|
|
87
|
+
? path_1.default.normalize(path_1.default.join(baseDir, configPathParam))
|
|
88
|
+
: outFile;
|
|
89
|
+
// Check for directory traversal attempts
|
|
90
|
+
const normalizedBaseDir = path_1.default.normalize(baseDir);
|
|
91
|
+
const relPath = path_1.default.relative(normalizedBaseDir, configPath);
|
|
92
|
+
// Ensure the config path is within the base directory and doesn't contain path traversal
|
|
93
|
+
if (relPath.startsWith('..') || path_1.default.isAbsolute(relPath)) {
|
|
94
|
+
return { isValid: false, error: 'Cannot perform directory traversal' };
|
|
95
|
+
}
|
|
96
|
+
return { isValid: true, configPath };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { isValid: false, error: 'Failed to validate config path' };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Sets up the API routes for the server
|
|
104
|
+
*/
|
|
105
|
+
function setupRoutes({ app, baseDir, outFile, key, }) {
|
|
106
|
+
// Root route
|
|
107
|
+
app.get('/', (_req, res) => {
|
|
108
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
109
|
+
res.send('JBrowse Admin Server');
|
|
110
|
+
});
|
|
111
|
+
// Update config route
|
|
112
|
+
app.post('/updateConfig', (req, res) => {
|
|
113
|
+
const { body } = req;
|
|
114
|
+
const config = body.config;
|
|
115
|
+
const validation = validateAndExtractParams({ req, key, baseDir, outFile });
|
|
116
|
+
if (!validation.isValid) {
|
|
117
|
+
res.status(401).setHeader('Content-Type', 'text/plain');
|
|
118
|
+
res.send(`Error: ${validation.error}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Remove adminKey from body before saving to config
|
|
122
|
+
if (body.adminKey) {
|
|
123
|
+
delete body.adminKey;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
fs_1.default.writeFileSync(validation.configPath, JSON.stringify(config, null, 2));
|
|
127
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
128
|
+
res.send('Config updated successfully');
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
res.status(500).setHeader('Content-Type', 'text/plain');
|
|
132
|
+
res.send('Error: Failed to update config');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// Get config route
|
|
136
|
+
app.get('/config', (req, res) => {
|
|
137
|
+
const validation = validateAndExtractParams({ req, key, baseDir, outFile });
|
|
138
|
+
if (!validation.isValid) {
|
|
139
|
+
res.status(401).setHeader('Content-Type', 'text/plain');
|
|
140
|
+
res.send(`Error: ${validation.error}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (fs_1.default.existsSync(validation.configPath)) {
|
|
145
|
+
const config = fs_1.default.readFileSync(validation.configPath, 'utf8');
|
|
146
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
147
|
+
res.send(config);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
res.status(404).setHeader('Content-Type', 'text/plain');
|
|
151
|
+
res.send('Error: Config file not found');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error('Error reading config:', error);
|
|
156
|
+
res.status(500).setHeader('Content-Type', 'text/plain');
|
|
157
|
+
res.send('Error: Failed to read config');
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Sets up the Express server with routes
|
|
163
|
+
*/
|
|
164
|
+
function setupServer({ baseDir, outFile, bodySizeLimit, }) {
|
|
165
|
+
// Create Express application
|
|
166
|
+
const app = (0, express_1.default)();
|
|
167
|
+
// Configure middleware
|
|
168
|
+
app.use(express_1.default.static(baseDir));
|
|
169
|
+
app.use((0, cors_1.default)());
|
|
170
|
+
app.use(express_1.default.json({ limit: bodySizeLimit }));
|
|
171
|
+
// Add error handling middleware
|
|
172
|
+
app.use((err, _req, res, next) => {
|
|
173
|
+
if (err) {
|
|
174
|
+
console.error('Server error:', err);
|
|
175
|
+
res.status(500).setHeader('Content-Type', 'text/plain');
|
|
176
|
+
res.send('Internal Server Error');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
next();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// Generate admin key and store it
|
|
183
|
+
const key = generateKey();
|
|
184
|
+
const keyPath = path_1.default.join(os_1.default.tmpdir(), `jbrowse-admin-${key}`);
|
|
185
|
+
try {
|
|
186
|
+
fs_1.default.writeFileSync(keyPath, key);
|
|
187
|
+
(0, utils_1.debug)(`Admin key stored at ${keyPath}`);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error(`Failed to write admin key to ${keyPath}:`, error.message);
|
|
191
|
+
// Continue anyway, as this is not critical
|
|
192
|
+
}
|
|
193
|
+
// Set up routes
|
|
194
|
+
setupRoutes({ app, baseDir, outFile, key });
|
|
195
|
+
return { app, key, keyPath };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Starts the server and sets up shutdown handlers
|
|
199
|
+
*/
|
|
200
|
+
function startServer({ app, port, key, outFile, keyPath, }) {
|
|
201
|
+
// Start the server
|
|
202
|
+
const server = app.listen(port, () => {
|
|
203
|
+
console.log(`Admin server started on port ${port}\n\n` +
|
|
204
|
+
`To access the admin interface, open your browser to:\n` +
|
|
205
|
+
`http://localhost:${port}?adminKey=${key}\n\n` +
|
|
206
|
+
`Admin key: ${key}\n` +
|
|
207
|
+
`Config file: ${outFile}\n\n` +
|
|
208
|
+
`To stop the server, press Ctrl+C`);
|
|
209
|
+
});
|
|
210
|
+
// Handle server errors
|
|
211
|
+
server.on('error', (error) => {
|
|
212
|
+
if (error.code === 'EADDRINUSE') {
|
|
213
|
+
console.error(`Error: Port ${port} is already in use`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.error('Server error:', error.message);
|
|
217
|
+
}
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
// Common shutdown handler
|
|
221
|
+
const shutdownHandler = () => {
|
|
222
|
+
console.log('\nShutting down admin server...');
|
|
223
|
+
server.close(() => {
|
|
224
|
+
// Clean up admin key file
|
|
225
|
+
try {
|
|
226
|
+
fs_1.default.unlinkSync(keyPath);
|
|
227
|
+
(0, utils_1.debug)(`Removed admin key file: ${keyPath}`);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// Ignore errors when cleaning up
|
|
231
|
+
}
|
|
232
|
+
process.exit(0);
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
// Handle server shutdown
|
|
236
|
+
process.on('SIGINT', shutdownHandler);
|
|
237
|
+
process.on('SIGTERM', shutdownHandler);
|
|
238
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.run = run;
|
|
4
|
+
const util_1 = require("util");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const admin_server_utils_1 = require("./admin-server-utils");
|
|
7
|
+
async function run(args) {
|
|
8
|
+
const options = {
|
|
9
|
+
help: {
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
short: 'h',
|
|
12
|
+
},
|
|
13
|
+
port: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
short: 'p',
|
|
16
|
+
description: 'Specified port to start the server on (default: 9090)',
|
|
17
|
+
},
|
|
18
|
+
root: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Path to the root of the JB2 installation',
|
|
21
|
+
},
|
|
22
|
+
bodySizeLimit: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Size limit of the update message (default: 25mb)',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const { values: flags } = (0, util_1.parseArgs)({
|
|
28
|
+
args,
|
|
29
|
+
options,
|
|
30
|
+
allowPositionals: true,
|
|
31
|
+
});
|
|
32
|
+
const description = 'Start up a small admin server for JBrowse configuration';
|
|
33
|
+
const examples = ['$ jbrowse admin-server', '$ jbrowse admin-server -p 8888'];
|
|
34
|
+
if (flags.help) {
|
|
35
|
+
(0, utils_1.printHelp)({
|
|
36
|
+
description,
|
|
37
|
+
examples,
|
|
38
|
+
usage: 'jbrowse admin-server [options]',
|
|
39
|
+
options,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const { root, bodySizeLimit = '25mb' } = flags;
|
|
44
|
+
const { outFile, baseDir } = await (0, admin_server_utils_1.setupConfigFile)({ root });
|
|
45
|
+
// Parse and validate port
|
|
46
|
+
const port = (0, admin_server_utils_1.parsePort)({ portStr: flags.port });
|
|
47
|
+
// Set up the Express server
|
|
48
|
+
const { app, key, keyPath } = (0, admin_server_utils_1.setupServer)({ baseDir, outFile, bodySizeLimit });
|
|
49
|
+
// Start the server and set up shutdown handlers
|
|
50
|
+
(0, admin_server_utils_1.startServer)({ app, port, key, outFile, keyPath });
|
|
51
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isValidJSON = isValidJSON;
|
|
7
|
+
exports.guessSequenceType = guessSequenceType;
|
|
8
|
+
exports.needLoadData = needLoadData;
|
|
9
|
+
exports.loadData = loadData;
|
|
10
|
+
exports.getAssembly = getAssembly;
|
|
11
|
+
exports.exists = exists;
|
|
12
|
+
exports.resolveTargetPath = resolveTargetPath;
|
|
13
|
+
exports.enhanceAssembly = enhanceAssembly;
|
|
14
|
+
exports.createDefaultConfig = createDefaultConfig;
|
|
15
|
+
exports.loadOrCreateConfig = loadOrCreateConfig;
|
|
16
|
+
exports.addAssemblyToConfig = addAssemblyToConfig;
|
|
17
|
+
exports.saveConfigAndReport = saveConfigAndReport;
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const utils_1 = require("../utils");
|
|
21
|
+
const { rename, copyFile, symlink } = fs_1.default.promises;
|
|
22
|
+
function isValidJSON(string) {
|
|
23
|
+
try {
|
|
24
|
+
JSON.parse(string);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function guessSequenceType(sequence) {
|
|
32
|
+
if (sequence.endsWith('.fa') ||
|
|
33
|
+
sequence.endsWith('.fna') ||
|
|
34
|
+
sequence.endsWith('.fasta')) {
|
|
35
|
+
return 'indexedFasta';
|
|
36
|
+
}
|
|
37
|
+
if (sequence.endsWith('.fa.gz') ||
|
|
38
|
+
sequence.endsWith('.fna.gz') ||
|
|
39
|
+
sequence.endsWith('.fasta.gz')) {
|
|
40
|
+
return 'bgzipFasta';
|
|
41
|
+
}
|
|
42
|
+
if (sequence.endsWith('.2bit')) {
|
|
43
|
+
return 'twoBit';
|
|
44
|
+
}
|
|
45
|
+
if (sequence.endsWith('.chrom.sizes')) {
|
|
46
|
+
return 'chromSizes';
|
|
47
|
+
}
|
|
48
|
+
if (sequence.endsWith('.json')) {
|
|
49
|
+
return 'custom';
|
|
50
|
+
}
|
|
51
|
+
if (isValidJSON(sequence)) {
|
|
52
|
+
return 'custom';
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Could not determine sequence type automatically, add --type to specify it');
|
|
55
|
+
}
|
|
56
|
+
function needLoadData(location) {
|
|
57
|
+
let locationUrl;
|
|
58
|
+
try {
|
|
59
|
+
locationUrl = new URL(location);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
if (locationUrl) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
async function loadData({ load, filePaths, destination, }) {
|
|
70
|
+
let locationUrl;
|
|
71
|
+
try {
|
|
72
|
+
locationUrl = new URL(filePaths[0]);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
if (locationUrl) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
switch (load) {
|
|
81
|
+
case 'copy': {
|
|
82
|
+
await Promise.all(filePaths.map(async (filePath) => {
|
|
83
|
+
if (!filePath) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return copyFile(filePath, path_1.default.join(path_1.default.dirname(destination), path_1.default.basename(filePath)));
|
|
87
|
+
}));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
case 'symlink': {
|
|
91
|
+
await Promise.all(filePaths.map(async (filePath) => {
|
|
92
|
+
if (!filePath) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return symlink(path_1.default.resolve(filePath), path_1.default.join(path_1.default.dirname(destination), path_1.default.basename(filePath)));
|
|
96
|
+
}));
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
case 'move': {
|
|
100
|
+
await Promise.all(filePaths.map(async (filePath) => {
|
|
101
|
+
if (!filePath) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return rename(filePath, path_1.default.join(path_1.default.dirname(destination), path_1.default.basename(filePath)));
|
|
105
|
+
}));
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
case 'inPlace': {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
async function getAssembly({ runFlags, argsSequence, target, }) {
|
|
115
|
+
let sequence;
|
|
116
|
+
if (needLoadData(argsSequence) && !runFlags.load) {
|
|
117
|
+
throw new Error('Please specify the loading operation for this file with --load copy|symlink|move|inPlace');
|
|
118
|
+
}
|
|
119
|
+
else if (!needLoadData(argsSequence) && runFlags.load) {
|
|
120
|
+
throw new Error('URL detected with --load flag. Please rerun the function without the --load flag');
|
|
121
|
+
}
|
|
122
|
+
let { name } = runFlags;
|
|
123
|
+
let { type } = runFlags;
|
|
124
|
+
if (type) {
|
|
125
|
+
(0, utils_1.debug)(`Type is: ${type}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
type = guessSequenceType(argsSequence);
|
|
129
|
+
(0, utils_1.debug)(`No type specified, guessing type: ${type}`);
|
|
130
|
+
}
|
|
131
|
+
if (name) {
|
|
132
|
+
(0, utils_1.debug)(`Name is: ${name}`);
|
|
133
|
+
}
|
|
134
|
+
switch (type) {
|
|
135
|
+
case 'indexedFasta': {
|
|
136
|
+
const { skipCheck, force, load, faiLocation } = runFlags;
|
|
137
|
+
let sequenceLocation = await (0, utils_1.resolveFileLocation)(argsSequence, !(skipCheck || force), load === 'inPlace');
|
|
138
|
+
(0, utils_1.debug)(`FASTA location resolved to: ${sequenceLocation}`);
|
|
139
|
+
let indexLocation = await (0, utils_1.resolveFileLocation)(faiLocation || `${argsSequence}.fai`, !(skipCheck || force), load === 'inPlace');
|
|
140
|
+
(0, utils_1.debug)(`FASTA index location resolved to: ${indexLocation}`);
|
|
141
|
+
if (!name) {
|
|
142
|
+
name = path_1.default.basename(sequenceLocation, sequenceLocation.endsWith('.fasta') ? '.fasta' : '.fa');
|
|
143
|
+
(0, utils_1.debug)(`Guessing name: ${name}`);
|
|
144
|
+
}
|
|
145
|
+
const loaded = load
|
|
146
|
+
? await loadData({
|
|
147
|
+
load,
|
|
148
|
+
filePaths: [sequenceLocation, indexLocation],
|
|
149
|
+
destination: target,
|
|
150
|
+
})
|
|
151
|
+
: false;
|
|
152
|
+
if (loaded) {
|
|
153
|
+
sequenceLocation = path_1.default.basename(sequenceLocation);
|
|
154
|
+
indexLocation = path_1.default.basename(indexLocation);
|
|
155
|
+
}
|
|
156
|
+
sequence = {
|
|
157
|
+
type: 'ReferenceSequenceTrack',
|
|
158
|
+
trackId: `${name}-ReferenceSequenceTrack`,
|
|
159
|
+
adapter: {
|
|
160
|
+
type: 'IndexedFastaAdapter',
|
|
161
|
+
fastaLocation: {
|
|
162
|
+
uri: sequenceLocation,
|
|
163
|
+
locationType: 'UriLocation',
|
|
164
|
+
},
|
|
165
|
+
faiLocation: { uri: indexLocation, locationType: 'UriLocation' },
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'bgzipFasta': {
|
|
171
|
+
let sequenceLocation = await (0, utils_1.resolveFileLocation)(argsSequence, !(runFlags.skipCheck || runFlags.force), runFlags.load === 'inPlace');
|
|
172
|
+
(0, utils_1.debug)(`compressed FASTA location resolved to: ${sequenceLocation}`);
|
|
173
|
+
let indexLocation = await (0, utils_1.resolveFileLocation)(runFlags.faiLocation || `${sequenceLocation}.fai`, !(runFlags.skipCheck || runFlags.force), runFlags.load === 'inPlace');
|
|
174
|
+
(0, utils_1.debug)(`compressed FASTA index location resolved to: ${indexLocation}`);
|
|
175
|
+
let bgzipIndexLocation = await (0, utils_1.resolveFileLocation)(runFlags.gziLocation || `${sequenceLocation}.gzi`, !(runFlags.skipCheck || runFlags.force), runFlags.load === 'inPlace');
|
|
176
|
+
(0, utils_1.debug)(`bgzip index location resolved to: ${bgzipIndexLocation}`);
|
|
177
|
+
if (!name) {
|
|
178
|
+
name = path_1.default.basename(sequenceLocation, sequenceLocation.endsWith('.fasta.gz') ? '.fasta.gz' : '.fa.gz');
|
|
179
|
+
(0, utils_1.debug)(`Guessing name: ${name}`);
|
|
180
|
+
}
|
|
181
|
+
const loaded = runFlags.load
|
|
182
|
+
? await loadData({
|
|
183
|
+
load: runFlags.load,
|
|
184
|
+
filePaths: [sequenceLocation, indexLocation, bgzipIndexLocation],
|
|
185
|
+
destination: target,
|
|
186
|
+
})
|
|
187
|
+
: false;
|
|
188
|
+
if (loaded) {
|
|
189
|
+
sequenceLocation = path_1.default.basename(sequenceLocation);
|
|
190
|
+
indexLocation = path_1.default.basename(indexLocation);
|
|
191
|
+
bgzipIndexLocation = path_1.default.basename(bgzipIndexLocation);
|
|
192
|
+
}
|
|
193
|
+
sequence = {
|
|
194
|
+
type: 'ReferenceSequenceTrack',
|
|
195
|
+
trackId: `${name}-ReferenceSequenceTrack`,
|
|
196
|
+
adapter: {
|
|
197
|
+
type: 'BgzipFastaAdapter',
|
|
198
|
+
fastaLocation: {
|
|
199
|
+
uri: sequenceLocation,
|
|
200
|
+
locationType: 'UriLocation',
|
|
201
|
+
},
|
|
202
|
+
faiLocation: { uri: indexLocation, locationType: 'UriLocation' },
|
|
203
|
+
gziLocation: {
|
|
204
|
+
uri: bgzipIndexLocation,
|
|
205
|
+
locationType: 'UriLocation',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'twoBit': {
|
|
212
|
+
let sequenceLocation = await (0, utils_1.resolveFileLocation)(argsSequence, !(runFlags.skipCheck || runFlags.force), runFlags.load === 'inPlace');
|
|
213
|
+
(0, utils_1.debug)(`2bit location resolved to: ${sequenceLocation}`);
|
|
214
|
+
if (!name) {
|
|
215
|
+
name = path_1.default.basename(sequenceLocation, '.2bit');
|
|
216
|
+
(0, utils_1.debug)(`Guessing name: ${name}`);
|
|
217
|
+
}
|
|
218
|
+
const loaded = runFlags.load
|
|
219
|
+
? await loadData({
|
|
220
|
+
load: runFlags.load,
|
|
221
|
+
filePaths: [sequenceLocation],
|
|
222
|
+
destination: target,
|
|
223
|
+
})
|
|
224
|
+
: false;
|
|
225
|
+
if (loaded) {
|
|
226
|
+
sequenceLocation = path_1.default.basename(sequenceLocation);
|
|
227
|
+
}
|
|
228
|
+
sequence = {
|
|
229
|
+
type: 'ReferenceSequenceTrack',
|
|
230
|
+
trackId: `${name}-ReferenceSequenceTrack`,
|
|
231
|
+
adapter: {
|
|
232
|
+
type: 'TwoBitAdapter',
|
|
233
|
+
twoBitLocation: {
|
|
234
|
+
uri: sequenceLocation,
|
|
235
|
+
locationType: 'UriLocation',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'chromSizes': {
|
|
242
|
+
let sequenceLocation = await (0, utils_1.resolveFileLocation)(argsSequence, !(runFlags.skipCheck || runFlags.force), runFlags.load === 'inPlace');
|
|
243
|
+
(0, utils_1.debug)(`chrom.sizes location resolved to: ${sequenceLocation}`);
|
|
244
|
+
if (!name) {
|
|
245
|
+
name = path_1.default.basename(sequenceLocation, '.chrom.sizes');
|
|
246
|
+
(0, utils_1.debug)(`Guessing name: ${name}`);
|
|
247
|
+
}
|
|
248
|
+
const loaded = runFlags.load
|
|
249
|
+
? await loadData({
|
|
250
|
+
load: runFlags.load,
|
|
251
|
+
filePaths: [sequenceLocation],
|
|
252
|
+
destination: target,
|
|
253
|
+
})
|
|
254
|
+
: false;
|
|
255
|
+
if (loaded) {
|
|
256
|
+
sequenceLocation = path_1.default.basename(sequenceLocation);
|
|
257
|
+
}
|
|
258
|
+
sequence = {
|
|
259
|
+
type: 'ReferenceSequenceTrack',
|
|
260
|
+
trackId: `${name}-ReferenceSequenceTrack`,
|
|
261
|
+
adapter: {
|
|
262
|
+
type: 'ChromSizesAdapter',
|
|
263
|
+
chromSizesLocation: {
|
|
264
|
+
uri: sequenceLocation,
|
|
265
|
+
locationType: 'UriLocation',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case 'custom': {
|
|
272
|
+
const adapter = await (0, utils_1.readInlineOrFileJson)(argsSequence);
|
|
273
|
+
(0, utils_1.debug)(`Custom adapter: ${JSON.stringify(adapter)}`);
|
|
274
|
+
if (!name) {
|
|
275
|
+
if (isValidJSON(argsSequence)) {
|
|
276
|
+
throw new Error('Must provide --name when using custom inline JSON sequence');
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
name = path_1.default.basename(argsSequence, '.json');
|
|
280
|
+
}
|
|
281
|
+
(0, utils_1.debug)(`Guessing name: ${name}`);
|
|
282
|
+
}
|
|
283
|
+
if (!('type' in adapter)) {
|
|
284
|
+
throw new Error(`No "type" specified in sequence adapter "${JSON.stringify(adapter)}"`);
|
|
285
|
+
}
|
|
286
|
+
sequence = {
|
|
287
|
+
type: 'ReferenceSequenceTrack',
|
|
288
|
+
trackId: `${name}-ReferenceSequenceTrack`,
|
|
289
|
+
adapter,
|
|
290
|
+
};
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { name, sequence };
|
|
295
|
+
}
|
|
296
|
+
async function exists(s) {
|
|
297
|
+
try {
|
|
298
|
+
await fs_1.default.promises.access(s, fs_1.default.constants.F_OK);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function resolveTargetPath(output) {
|
|
306
|
+
if (!(await exists(output))) {
|
|
307
|
+
const dir = output.endsWith('.json') ? path_1.default.dirname(output) : output;
|
|
308
|
+
await fs_1.default.promises.mkdir(dir, { recursive: true });
|
|
309
|
+
}
|
|
310
|
+
let isDir = false;
|
|
311
|
+
try {
|
|
312
|
+
isDir = fs_1.default.statSync(output).isDirectory();
|
|
313
|
+
}
|
|
314
|
+
catch (e) { }
|
|
315
|
+
return isDir ? path_1.default.join(output, 'config.json') : output;
|
|
316
|
+
}
|
|
317
|
+
async function enhanceAssembly(assembly, runFlags) {
|
|
318
|
+
const enhancedAssembly = { ...assembly };
|
|
319
|
+
if (runFlags.alias?.length) {
|
|
320
|
+
(0, utils_1.debug)(`Adding assembly aliases: ${runFlags.alias}`);
|
|
321
|
+
enhancedAssembly.aliases = runFlags.alias;
|
|
322
|
+
}
|
|
323
|
+
if (runFlags.refNameColors) {
|
|
324
|
+
enhancedAssembly.refNameColors = runFlags.refNameColors
|
|
325
|
+
.split(',')
|
|
326
|
+
.map((color) => color.trim());
|
|
327
|
+
}
|
|
328
|
+
if (runFlags.refNameAliases) {
|
|
329
|
+
if (runFlags.refNameAliasesType &&
|
|
330
|
+
runFlags.refNameAliasesType === 'custom') {
|
|
331
|
+
const refNameAliasesConfig = await (0, utils_1.readInlineOrFileJson)(runFlags.refNameAliases);
|
|
332
|
+
if (!refNameAliasesConfig.type) {
|
|
333
|
+
throw new Error(`No "type" specified in refNameAliases adapter "${JSON.stringify(refNameAliasesConfig)}"`);
|
|
334
|
+
}
|
|
335
|
+
(0, utils_1.debug)(`Adding custom refNameAliases config: ${JSON.stringify(refNameAliasesConfig)}`);
|
|
336
|
+
enhancedAssembly.refNameAliases = {
|
|
337
|
+
adapter: refNameAliasesConfig,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const refNameAliasesLocation = await (0, utils_1.resolveFileLocation)(runFlags.refNameAliases, !(runFlags.skipCheck || runFlags.force), runFlags.load === 'inPlace');
|
|
342
|
+
(0, utils_1.debug)(`refName aliases file location resolved to: ${refNameAliasesLocation}`);
|
|
343
|
+
enhancedAssembly.refNameAliases = {
|
|
344
|
+
adapter: {
|
|
345
|
+
type: 'RefNameAliasAdapter',
|
|
346
|
+
location: {
|
|
347
|
+
uri: refNameAliasesLocation,
|
|
348
|
+
locationType: 'UriLocation',
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (runFlags.displayName) {
|
|
355
|
+
enhancedAssembly.displayName = runFlags.displayName;
|
|
356
|
+
}
|
|
357
|
+
return enhancedAssembly;
|
|
358
|
+
}
|
|
359
|
+
function createDefaultConfig() {
|
|
360
|
+
return {
|
|
361
|
+
assemblies: [],
|
|
362
|
+
configuration: {},
|
|
363
|
+
connections: [],
|
|
364
|
+
defaultSession: {
|
|
365
|
+
name: 'New Session',
|
|
366
|
+
},
|
|
367
|
+
tracks: [],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function loadOrCreateConfig(target) {
|
|
371
|
+
const defaultConfig = createDefaultConfig();
|
|
372
|
+
if (fs_1.default.existsSync(target)) {
|
|
373
|
+
(0, utils_1.debug)(`Found existing config file ${target}`);
|
|
374
|
+
return {
|
|
375
|
+
...defaultConfig,
|
|
376
|
+
...(await (0, utils_1.readJsonFile)(target)),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
(0, utils_1.debug)(`Creating config file ${target}`);
|
|
381
|
+
return { ...defaultConfig };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function addAssemblyToConfig({ config, assembly, runFlags, }) {
|
|
385
|
+
const updatedConfig = { ...config };
|
|
386
|
+
if (!updatedConfig.assemblies) {
|
|
387
|
+
updatedConfig.assemblies = [];
|
|
388
|
+
}
|
|
389
|
+
const idx = updatedConfig.assemblies.findIndex(configAssembly => configAssembly.name === assembly.name);
|
|
390
|
+
if (idx !== -1) {
|
|
391
|
+
(0, utils_1.debug)(`Found existing assembly ${assembly.name} in configuration`);
|
|
392
|
+
if (runFlags.overwrite || runFlags.force) {
|
|
393
|
+
(0, utils_1.debug)(`Overwriting assembly ${assembly.name} in configuration`);
|
|
394
|
+
updatedConfig.assemblies[idx] = assembly;
|
|
395
|
+
return { config: updatedConfig, wasOverwritten: true };
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
throw new Error(`Cannot add assembly with name ${assembly.name}, an assembly with that name already exists`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
updatedConfig.assemblies.push(assembly);
|
|
403
|
+
return { config: updatedConfig, wasOverwritten: false };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function saveConfigAndReport({ config, target, assembly, wasOverwritten, }) {
|
|
407
|
+
(0, utils_1.debug)(`Writing configuration to file ${target}`);
|
|
408
|
+
await (0, utils_1.writeJsonFile)(target, config);
|
|
409
|
+
console.log(`${wasOverwritten ? 'Overwrote' : 'Added'} assembly "${assembly.name}" ${wasOverwritten ? 'in' : 'to'} ${target}`);
|
|
410
|
+
}
|