@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
package/dist/utils.js
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
|
-
import { promises as fsPromises } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import
|
|
4
|
-
import fetch from
|
|
1
|
+
import { promises as fsPromises, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import zlib from 'node:zlib';
|
|
4
|
+
import fetch from './cliFetch.js';
|
|
5
|
+
export function parseCommaSeparatedString(value) {
|
|
6
|
+
return (value
|
|
7
|
+
?.split(',')
|
|
8
|
+
.map(s => s.trim())
|
|
9
|
+
.filter(Boolean) ?? []);
|
|
10
|
+
}
|
|
11
|
+
// throws a uniform "missing argument" error (with the same usage string passed
|
|
12
|
+
// to printHelp) when a required positional is absent, and narrows the value to
|
|
13
|
+
// string for the caller
|
|
14
|
+
export function requirePositional(value, name, usage) {
|
|
15
|
+
if (!value) {
|
|
16
|
+
throw new Error(`Missing required argument: ${name}\nUsage: ${usage}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// a JSON object, as opposed to a string/number/array/null. Parsed JSON reaches
|
|
20
|
+
// several commands as `unknown`, and the failure mode of not checking is that
|
|
21
|
+
// spreading a string yields an object of numeric keys.
|
|
22
|
+
export function isRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
export function ignoreNotFound(promise) {
|
|
26
|
+
return promise.catch((err) => {
|
|
27
|
+
if (err.code !== 'ENOENT') {
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
5
33
|
export function debug(message) {
|
|
6
34
|
if (process.env.DEBUG) {
|
|
7
35
|
console.log(`DEBUG: ${message}`);
|
|
@@ -9,58 +37,62 @@ export function debug(message) {
|
|
|
9
37
|
}
|
|
10
38
|
export async function resolveConfigPath(target, out) {
|
|
11
39
|
const output = target || out || '.';
|
|
12
|
-
|
|
13
|
-
|
|
40
|
+
// stat (not lstat) so a symlinked install directory resolves to its
|
|
41
|
+
// config.json rather than being treated as the config file itself. A path
|
|
42
|
+
// that isn't there at all is resolved from its name instead of rejected here,
|
|
43
|
+
// so the caller's readConfigFile reports which config was missing and how to
|
|
44
|
+
// make one rather than this leaking a bare stat ENOENT
|
|
45
|
+
const stat = await ignoreNotFound(fsPromises.stat(output));
|
|
46
|
+
const isDir = stat ? stat.isDirectory() : !output.endsWith('.json');
|
|
47
|
+
return isDir ? path.join(output, 'config.json') : output;
|
|
14
48
|
}
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
// `-` as a file argument means stdin, so a JSON hunk can be piped in rather than
|
|
50
|
+
// staged as a file: `jq … | jbrowse add-track-json -`
|
|
51
|
+
export const STDIN_ARG = '-';
|
|
52
|
+
let stdinConsumed = false;
|
|
53
|
+
function readStdin() {
|
|
54
|
+
// reading fd 0 twice yields nothing the second time, which would surface as a
|
|
55
|
+
// baffling JSON parse error rather than the mistake it is
|
|
56
|
+
if (stdinConsumed) {
|
|
57
|
+
throw new Error('stdin ("-") can only be read once per command');
|
|
58
|
+
}
|
|
59
|
+
stdinConsumed = true;
|
|
60
|
+
return readFileSync(0, 'utf8');
|
|
17
61
|
}
|
|
18
62
|
export async function readJsonFile(location) {
|
|
19
|
-
const contents =
|
|
20
|
-
|
|
63
|
+
const contents = location === STDIN_ARG
|
|
64
|
+
? readStdin()
|
|
65
|
+
: await fsPromises.readFile(location, { encoding: 'utf8' });
|
|
66
|
+
return JSON.parse(contents);
|
|
67
|
+
}
|
|
68
|
+
// reads the config of an existing install. Running a command from the wrong
|
|
69
|
+
// directory is the most common mistake, and a bare ENOENT does not say which
|
|
70
|
+
// directory was searched or what to do about it
|
|
71
|
+
export async function readConfigFile(location) {
|
|
72
|
+
// catching the code rather than testing the parsed value keeps a config that
|
|
73
|
+
// legitimately parses to something falsy from reporting itself as missing
|
|
74
|
+
return readJsonFile(location).catch((error) => {
|
|
75
|
+
if (error.code === 'ENOENT') {
|
|
76
|
+
throw new Error(`No JBrowse config found at ${path.resolve(location)}\n` +
|
|
77
|
+
'Run "jbrowse create <dir>" to make a new installation, or point at an existing one with --out <dir>');
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
21
81
|
}
|
|
22
82
|
export async function writeJsonFile(location, contents) {
|
|
23
83
|
debug(`Writing JSON file to ${process.cwd()} ${location}`);
|
|
24
84
|
return fsPromises.writeFile(location, JSON.stringify(contents, null, 2));
|
|
25
85
|
}
|
|
26
|
-
export async function resolveFileLocation(location, check = true, inPlace = false) {
|
|
27
|
-
let locationUrl;
|
|
28
|
-
try {
|
|
29
|
-
locationUrl = new URL(location);
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
// ignore
|
|
33
|
-
}
|
|
34
|
-
if (locationUrl) {
|
|
35
|
-
if (check) {
|
|
36
|
-
const response = await fetch(locationUrl, { method: 'HEAD' });
|
|
37
|
-
if (!response.ok) {
|
|
38
|
-
throw new Error(`${locationUrl} result ${response.statusText}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return locationUrl.href;
|
|
42
|
-
}
|
|
43
|
-
let locationPath;
|
|
44
|
-
try {
|
|
45
|
-
locationPath = check ? await fsPromises.realpath(location) : location;
|
|
46
|
-
}
|
|
47
|
-
catch (e) {
|
|
48
|
-
// ignore
|
|
49
|
-
}
|
|
50
|
-
if (locationPath) {
|
|
51
|
-
const filePath = path.relative(process.cwd(), locationPath);
|
|
52
|
-
if (inPlace && filePath.startsWith('..')) {
|
|
53
|
-
console.warn(`Location ${filePath} is not in the JBrowse directory. Make sure it is still in your server directory.`);
|
|
54
|
-
}
|
|
55
|
-
return inPlace ? location : filePath;
|
|
56
|
-
}
|
|
57
|
-
throw new Error(`Could not resolve to a file or a URL: "${location}"`);
|
|
58
|
-
}
|
|
59
86
|
export async function readInlineOrFileJson(inlineOrFileName) {
|
|
87
|
+
// checked before the inline attempt rather than falling through it, so the
|
|
88
|
+
// intent is explicit (JSON.parse('-') happens to throw, but not by design)
|
|
89
|
+
if (inlineOrFileName === STDIN_ARG) {
|
|
90
|
+
return readJsonFile(STDIN_ARG);
|
|
91
|
+
}
|
|
60
92
|
let result;
|
|
61
93
|
// see if it's inline JSON
|
|
62
94
|
try {
|
|
63
|
-
result =
|
|
95
|
+
result = JSON.parse(inlineOrFileName);
|
|
64
96
|
}
|
|
65
97
|
catch (error) {
|
|
66
98
|
debug(`Not valid inline JSON, attempting to parse as filename: '${inlineOrFileName}'`);
|
|
@@ -69,115 +101,264 @@ export async function readInlineOrFileJson(inlineOrFileName) {
|
|
|
69
101
|
}
|
|
70
102
|
return result;
|
|
71
103
|
}
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
async function fetchGithubVersions() {
|
|
105
|
+
const versions = [];
|
|
74
106
|
for await (const iter of fetchVersions()) {
|
|
75
|
-
versions
|
|
107
|
+
versions.push(...iter);
|
|
76
108
|
}
|
|
77
109
|
return versions;
|
|
78
110
|
}
|
|
79
|
-
|
|
111
|
+
// Authenticated when the environment offers a token, because GitHub's anonymous
|
|
112
|
+
// API allows 60 requests an hour PER IP and CI runners share addresses: a
|
|
113
|
+
// `jbrowse create --tag v4.3.0` step fails partway through an otherwise green
|
|
114
|
+
// run, on nobody's diff, at whatever hour someone else's job exhausted the
|
|
115
|
+
// budget. jbrowse-plugin-protein3d and -msaview both lost a pinned-host e2e leg
|
|
116
|
+
// to it within a minute of each other on 2026-08-17. GITHUB_TOKEN is already in
|
|
117
|
+
// the environment of every Actions job, and raises the ceiling to 5000.
|
|
118
|
+
function githubToken() {
|
|
119
|
+
return process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
120
|
+
}
|
|
121
|
+
// fetch + parse JSON from the GitHub API, throwing on a non-ok response. The
|
|
122
|
+
// lone cast localizes the unavoidable response.json(): Promise<unknown>
|
|
123
|
+
async function fetchJson(url) {
|
|
124
|
+
const token = githubToken();
|
|
125
|
+
const response = await fetch(url, token ? { headers: { authorization: `Bearer ${token}` } } : {});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
// 403 unauthenticated is nearly always the rate limit rather than anything
|
|
128
|
+
// about this url, and the caller above turns a throw into "could not find
|
|
129
|
+
// version", which sends the reader looking for a version that is right
|
|
130
|
+
// there. Say which it is.
|
|
131
|
+
throw new Error(response.status === 403 && !token
|
|
132
|
+
? `HTTP 403 fetching ${url} -- probably GitHub's 60/hour anonymous rate limit. Set GITHUB_TOKEN to raise it.`
|
|
133
|
+
: `HTTP ${response.status} fetching ${url}`);
|
|
134
|
+
}
|
|
135
|
+
return response.json();
|
|
136
|
+
}
|
|
137
|
+
// the jbrowse-web build is the asset we unpack; a release can have no matching
|
|
138
|
+
// asset if it was just uploaded or its build failed
|
|
139
|
+
function findWebAssetUrl(release) {
|
|
140
|
+
return release.assets?.find(f => f.name.includes('jbrowse-web'))
|
|
141
|
+
?.browser_download_url;
|
|
142
|
+
}
|
|
143
|
+
async function getLatest() {
|
|
80
144
|
for await (const versions of fetchVersions()) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const file = first.assets.find(f => f.name.includes('jbrowse-web'))?.browser_download_url;
|
|
89
|
-
if (!file) {
|
|
145
|
+
const release = versions.find(r => !r.prerelease && r.assets?.length);
|
|
146
|
+
if (release) {
|
|
147
|
+
const file = findWebAssetUrl(release);
|
|
148
|
+
if (file) {
|
|
149
|
+
return file;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
90
152
|
throw new Error('no jbrowse-web download found');
|
|
91
153
|
}
|
|
92
|
-
return file;
|
|
93
154
|
}
|
|
94
155
|
}
|
|
95
156
|
throw new Error('no version tags found');
|
|
96
157
|
}
|
|
97
|
-
|
|
158
|
+
async function* fetchVersions() {
|
|
98
159
|
let page = 1;
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
160
|
+
let done = false;
|
|
161
|
+
while (!done) {
|
|
162
|
+
const result = await fetchJson(`https://api.github.com/repos/GMOD/jbrowse-components/releases?page=${page}`);
|
|
163
|
+
if (result.length === 0) {
|
|
164
|
+
done = true;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
105
167
|
yield result.filter(release => release.tag_name.startsWith('v'));
|
|
106
168
|
page++;
|
|
107
169
|
}
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function getTag(tag) {
|
|
173
|
+
const release = await fetchJson(`https://api.github.com/repos/GMOD/jbrowse-components/releases/tags/${tag}`);
|
|
174
|
+
const file = findWebAssetUrl(release);
|
|
175
|
+
if (file) {
|
|
176
|
+
return file;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
throw new Error('Could not find version specified. Use --listVersions to see all available versions');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// The prefix a branch build is deployed under is slash-free. push.yml collapses
|
|
183
|
+
// `/` to `-` before it syncs (`SAFE_REF="${REF_NAME//\//-}"`) and then asserts
|
|
184
|
+
// the result is [A-Za-z0-9_.-], because the prefix is an S3 path and a nested
|
|
185
|
+
// branch name would otherwise open a directory in it. The zip inside is named
|
|
186
|
+
// from the same collapsed ref.
|
|
187
|
+
//
|
|
188
|
+
// So the collapse has to happen on this side too, or `--branch port/foo` asks
|
|
189
|
+
// for code/jb2/port/foo/jbrowse-web-port/foo.zip and 404s — for a build that
|
|
190
|
+
// was deployed, and is sitting at code/jb2/port-foo/. Slashes are the normal
|
|
191
|
+
// case for a branch name here (port/*, dependabot/*, anyone's feature/*).
|
|
192
|
+
function getBranch(branch) {
|
|
193
|
+
const safeBranch = branch.replaceAll('/', '-');
|
|
194
|
+
return `https://s3.amazonaws.com/jbrowse.org/code/jb2/${safeBranch}/jbrowse-web-${safeBranch}.zip`;
|
|
195
|
+
}
|
|
196
|
+
// resolves the JBrowse release download URL from the create/upgrade flags,
|
|
197
|
+
// preferring an explicit --url, then --nightly, --branch, and finally --tag
|
|
198
|
+
// (or the latest release)
|
|
199
|
+
async function resolveReleaseUrl({ url, nightly, branch, tag }) {
|
|
200
|
+
return url
|
|
201
|
+
? url
|
|
202
|
+
: nightly
|
|
203
|
+
? getBranch('main')
|
|
204
|
+
: branch
|
|
205
|
+
? getBranch(branch)
|
|
206
|
+
: tag
|
|
207
|
+
? getTag(tag)
|
|
208
|
+
: getLatest();
|
|
209
|
+
}
|
|
210
|
+
// shared by create/upgrade: resolve the release URL then download it. Returns
|
|
211
|
+
// both so the caller can log which URL was unpacked.
|
|
212
|
+
export async function downloadRelease(flags) {
|
|
213
|
+
const locationUrl = await resolveReleaseUrl(flags);
|
|
214
|
+
const archive = await fetchReleaseArchive(locationUrl, !!flags.url);
|
|
215
|
+
return { locationUrl, archive };
|
|
216
|
+
}
|
|
217
|
+
export async function printVersions() {
|
|
218
|
+
const versions = (await fetchGithubVersions()).map(v => v.tag_name);
|
|
219
|
+
console.log(`All JBrowse versions:\n${versions.join('\n')}`);
|
|
220
|
+
}
|
|
221
|
+
async function fetchReleaseArchive(locationUrl, validateZipContentType) {
|
|
222
|
+
console.log(`Fetching ${locationUrl}...`);
|
|
223
|
+
const response = await fetch(locationUrl);
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`HTTP ${response.status} fetching ${locationUrl}: ${response.statusText}`);
|
|
226
|
+
}
|
|
227
|
+
const type = response.headers.get('content-type');
|
|
228
|
+
if (validateZipContentType &&
|
|
229
|
+
type !== 'application/zip' &&
|
|
230
|
+
type !== 'application/octet-stream') {
|
|
231
|
+
throw new Error('The URL provided does not seem to be a JBrowse installation URL');
|
|
232
|
+
}
|
|
233
|
+
return Buffer.from(await response.arrayBuffer());
|
|
234
|
+
}
|
|
235
|
+
// ZIP record signatures (little-endian)
|
|
236
|
+
const EOCD_SIG = 0x06054b50; // end of central directory
|
|
237
|
+
const CDH_SIG = 0x02014b50; // central directory header
|
|
238
|
+
// Walks the central directory rather than the local headers, because its
|
|
239
|
+
// offsets/sizes stay accurate even when a local header defers them to a data
|
|
240
|
+
// descriptor. Yielding entries keeps the pointer arithmetic in one place, so
|
|
241
|
+
// everything downstream works in terms of entries.
|
|
242
|
+
function* centralDirectoryEntries(archive) {
|
|
243
|
+
const eocd = findEndOfCentralDirectory(archive);
|
|
244
|
+
const entryCount = archive.readUInt16LE(eocd + 10);
|
|
245
|
+
let ptr = archive.readUInt32LE(eocd + 16);
|
|
246
|
+
for (let i = 0; i < entryCount; i++) {
|
|
247
|
+
if (archive.readUInt32LE(ptr) !== CDH_SIG) {
|
|
248
|
+
throw new Error('Corrupt ZIP: bad central directory header');
|
|
110
249
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
250
|
+
const nameLen = archive.readUInt16LE(ptr + 28);
|
|
251
|
+
const extraLen = archive.readUInt16LE(ptr + 30);
|
|
252
|
+
const commentLen = archive.readUInt16LE(ptr + 32);
|
|
253
|
+
yield {
|
|
254
|
+
name: archive.toString('utf8', ptr + 46, ptr + 46 + nameLen),
|
|
255
|
+
method: archive.readUInt16LE(ptr + 10),
|
|
256
|
+
compressedSize: archive.readUInt32LE(ptr + 20),
|
|
257
|
+
localOffset: archive.readUInt32LE(ptr + 42),
|
|
258
|
+
};
|
|
259
|
+
ptr += 46 + nameLen + extraLen + commentLen;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function inflateEntry(archive, { localOffset, compressedSize, method }) {
|
|
263
|
+
// the local header's name/extra lengths can differ from the central one, so
|
|
264
|
+
// the data offset has to be computed from the local header
|
|
265
|
+
const nameLen = archive.readUInt16LE(localOffset + 26);
|
|
266
|
+
const extraLen = archive.readUInt16LE(localOffset + 28);
|
|
267
|
+
const dataStart = localOffset + 30 + nameLen + extraLen;
|
|
268
|
+
const raw = archive.subarray(dataStart, dataStart + compressedSize);
|
|
269
|
+
return method === 0 ? raw : zlib.inflateRawSync(raw);
|
|
270
|
+
}
|
|
271
|
+
// Extracts a ZIP archive (a JBrowse web build) into destPath with the built-in
|
|
272
|
+
// node:zlib. Replaces the `decompress` dependency and its large, unmaintained
|
|
273
|
+
// transitive tree without pulling in a new one.
|
|
274
|
+
export async function extractZip(archive, destPath) {
|
|
275
|
+
await Promise.all([...centralDirectoryEntries(archive)]
|
|
276
|
+
// directory entries (trailing slash) carry no file data; the files below
|
|
277
|
+
// create their own parent dirs
|
|
278
|
+
.filter(entry => !entry.name.endsWith('/'))
|
|
279
|
+
.map(entry => writeZipEntry(destPath, entry.name, inflateEntry(archive, entry))));
|
|
280
|
+
}
|
|
281
|
+
async function writeZipEntry(destPath, name, data) {
|
|
282
|
+
const outPath = path.join(destPath, name);
|
|
283
|
+
// guard against zip-slip: a crafted entry name (e.g. ../../etc/foo) must not
|
|
284
|
+
// let path.join escape destPath, since --url accepts arbitrary archives
|
|
285
|
+
const rel = path.relative(destPath, outPath);
|
|
286
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
287
|
+
throw new Error(`Corrupt ZIP: entry escapes destination directory: ${name}`);
|
|
288
|
+
}
|
|
289
|
+
await fsPromises.mkdir(path.dirname(outPath), { recursive: true });
|
|
290
|
+
await fsPromises.writeFile(outPath, data);
|
|
291
|
+
}
|
|
292
|
+
function findEndOfCentralDirectory(archive) {
|
|
293
|
+
// the EOCD sits at the end, after an optional comment of up to 64KB, so scan
|
|
294
|
+
// backwards for its signature
|
|
295
|
+
for (let i = archive.length - 22; i >= 0; i--) {
|
|
296
|
+
if (archive.readUInt32LE(i) === EOCD_SIG) {
|
|
297
|
+
return i;
|
|
120
298
|
}
|
|
121
|
-
return file;
|
|
122
299
|
}
|
|
123
|
-
throw new Error(
|
|
300
|
+
throw new Error('Corrupt ZIP: no end-of-central-directory record');
|
|
124
301
|
}
|
|
125
|
-
|
|
126
|
-
|
|
302
|
+
// help text is authored as one long string where \n\n is a paragraph break and a
|
|
303
|
+
// bare \n is insignificant whitespace, so unfold it back into paragraphs
|
|
304
|
+
function paragraphs(text) {
|
|
305
|
+
return text.split('\n\n').map(p => p.replaceAll('\n', ' '));
|
|
127
306
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
307
|
+
// greedily pack words into lines no wider than `width`. A word longer than the
|
|
308
|
+
// width gets a line to itself rather than being broken.
|
|
309
|
+
function wrapParagraph(paragraph, width) {
|
|
310
|
+
if (paragraph.length <= width) {
|
|
311
|
+
return [paragraph];
|
|
312
|
+
}
|
|
134
313
|
const lines = [];
|
|
135
|
-
for (const
|
|
136
|
-
|
|
137
|
-
|
|
314
|
+
for (const word of paragraph.split(' ')) {
|
|
315
|
+
const last = lines.at(-1);
|
|
316
|
+
if (last !== undefined && last.length + 1 + word.length <= width) {
|
|
317
|
+
lines[lines.length - 1] = `${last} ${word}`;
|
|
138
318
|
}
|
|
139
319
|
else {
|
|
140
|
-
|
|
141
|
-
let current = '';
|
|
142
|
-
for (const word of words) {
|
|
143
|
-
if (current.length + word.length + 1 <= width) {
|
|
144
|
-
current += (current ? ' ' : '') + word;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
if (current) {
|
|
148
|
-
lines.push(current);
|
|
149
|
-
}
|
|
150
|
-
current = word;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (current) {
|
|
154
|
-
lines.push(current);
|
|
155
|
-
}
|
|
320
|
+
lines.push(word);
|
|
156
321
|
}
|
|
157
322
|
}
|
|
158
|
-
return lines
|
|
323
|
+
return lines;
|
|
324
|
+
}
|
|
325
|
+
function wrapText(text, width, indent) {
|
|
326
|
+
return (paragraphs(text)
|
|
327
|
+
// a blank line separates paragraphs
|
|
328
|
+
.flatMap((p, i) => (i === 0 ? [] : ['']).concat(wrapParagraph(p, width)))
|
|
329
|
+
// indent continuation lines, but leave the blank separators empty rather
|
|
330
|
+
// than filling them with trailing indent whitespace
|
|
331
|
+
.map((line, i) => (i === 0 || !line ? line : indent + line))
|
|
332
|
+
.join('\n'));
|
|
333
|
+
}
|
|
334
|
+
// one rendered `-x, --name description [choices: ...] [default: ...]` line, with
|
|
335
|
+
// the description wrapped under the name column
|
|
336
|
+
function formatOption(name, opt, termWidth) {
|
|
337
|
+
const prefix = opt.short ? ` -${opt.short}, ` : ' '.repeat(6);
|
|
338
|
+
const namePadded = `--${name}`.padEnd(22, ' ');
|
|
339
|
+
const indent = ' '.repeat(prefix.length + namePadded.length + 1);
|
|
340
|
+
const desc = [
|
|
341
|
+
// every command declares a bare help flag; give it uniform wording so the
|
|
342
|
+
// rendered `-h, --help` line is never blank
|
|
343
|
+
opt.description ?? (name === 'help' ? 'Show help' : ''),
|
|
344
|
+
opt.choices && ` [choices: ${opt.choices.join(', ')}]`,
|
|
345
|
+
opt.default !== undefined && ` [default: ${opt.default}]`,
|
|
346
|
+
]
|
|
347
|
+
.filter(Boolean)
|
|
348
|
+
.join('');
|
|
349
|
+
const wrapped = desc ? wrapText(desc, termWidth - indent.length, indent) : '';
|
|
350
|
+
return `${prefix}${namePadded} ${wrapped}`.trimEnd();
|
|
159
351
|
}
|
|
160
|
-
export function printHelp({ description, options, examples, usage, }) {
|
|
352
|
+
export function printHelp({ description, options, examples, notes, usage, }) {
|
|
161
353
|
const termWidth = process.stdout.columns || 80;
|
|
162
|
-
console.log(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let desc = opt.description || '';
|
|
173
|
-
if (opt.choices) {
|
|
174
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
175
|
-
}
|
|
176
|
-
if (opt.default !== undefined) {
|
|
177
|
-
desc += ` [default: ${opt.default}]`;
|
|
178
|
-
}
|
|
179
|
-
const wrapped = desc ? wrapText(desc, descWidth, indent) : '';
|
|
180
|
-
console.log(`${prefix}${namePadded} ${wrapped}\n`);
|
|
181
|
-
}
|
|
182
|
-
console.log(examples.join('\n'));
|
|
354
|
+
console.log([
|
|
355
|
+
wrapText(description, termWidth, ''),
|
|
356
|
+
`\nUsage: ${usage || 'jbrowse <command> [options]'}`,
|
|
357
|
+
'\nOptions:',
|
|
358
|
+
// a blank line after each option, so long wrapped descriptions stay
|
|
359
|
+
// visually separated
|
|
360
|
+
...Object.entries(options).map(([name, opt]) => `${formatOption(name, opt, termWidth)}\n`),
|
|
361
|
+
...(notes ? [`Notes:\n\n${wrapText(notes, termWidth, '')}\n`] : []),
|
|
362
|
+
...(examples.length ? ['Examples:\n', examples.join('\n')] : []),
|
|
363
|
+
].join('\n'));
|
|
183
364
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '
|
|
1
|
+
export const version = '5.0.0-beta.10';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-beta.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A command line tool for working with JBrowse 2",
|
|
6
6
|
"keywords": [
|
|
@@ -21,31 +21,25 @@
|
|
|
21
21
|
"jbrowse": "./dist/bin.js"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"/bin",
|
|
25
|
-
"/bundle",
|
|
26
24
|
"/dist"
|
|
27
25
|
],
|
|
28
26
|
"engines": {
|
|
29
27
|
"node": ">=18.3.0"
|
|
30
28
|
},
|
|
31
29
|
"dependencies": {
|
|
30
|
+
"ajv": "^8.20.0",
|
|
32
31
|
"cli-progress": "^3.12.0",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"json-parse-better-errors": "^1.0.2",
|
|
39
|
-
"tmp": "^0.2.5",
|
|
40
|
-
"@jbrowse/text-indexing-core": "^4.3.0"
|
|
32
|
+
"serve-handler": "^6.1.7",
|
|
33
|
+
"tmp": "^0.2.7",
|
|
34
|
+
"@jbrowse/add-track-core": "^5.0.0-beta.10",
|
|
35
|
+
"@jbrowse/cigar-utils": "^5.0.0-beta.10",
|
|
36
|
+
"@jbrowse/text-indexing-core": "^5.0.0-beta.10"
|
|
41
37
|
},
|
|
42
38
|
"publishConfig": {
|
|
43
39
|
"access": "public"
|
|
44
40
|
},
|
|
45
41
|
"scripts": {
|
|
46
|
-
"
|
|
47
|
-
"build": "tsc && webpack -c webpack.config.mjs",
|
|
48
|
-
"clean": "rimraf bundle",
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
49
43
|
"generate-readme": "./generate_readme.sh > README.md"
|
|
50
44
|
}
|
|
51
45
|
}
|
package/bin/run
DELETED