@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
package/dist/utils.js CHANGED
@@ -1,7 +1,35 @@
1
- import { promises as fsPromises } from 'fs';
2
- import path from 'path';
3
- import parseJSON from 'json-parse-better-errors';
4
- import fetch from "./cliFetch.js";
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
- const stat = await fsPromises.lstat(output);
13
- return stat.isDirectory() ? `${output}/config.json` : output;
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
- export async function readFile(location) {
16
- return fsPromises.readFile(location, { encoding: 'utf8' });
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 = await fsPromises.readFile(location, { encoding: 'utf8' });
20
- return parseJSON(contents);
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 = parseJSON(inlineOrFileName);
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
- export async function fetchGithubVersions() {
73
- let versions = [];
104
+ async function fetchGithubVersions() {
105
+ const versions = [];
74
106
  for await (const iter of fetchVersions()) {
75
- versions = [...versions, ...iter];
107
+ versions.push(...iter);
76
108
  }
77
109
  return versions;
78
110
  }
79
- export async function getLatest() {
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
- // if a release was just uploaded, or an erroneous build was made then it
82
- // might have no build asset
83
- const nonprerelease = versions
84
- .filter(release => !release.prerelease)
85
- .find(release => release.assets?.length);
86
- const first = nonprerelease;
87
- if (first?.assets) {
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
- export async function* fetchVersions() {
158
+ async function* fetchVersions() {
98
159
  let page = 1;
99
- let result;
100
- do {
101
- const url = `https://api.github.com/repos/GMOD/jbrowse-components/releases?page=${page}`;
102
- const response = await fetch(url);
103
- if (response.ok) {
104
- result = (await response.json());
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
- else {
109
- throw new Error(`HTTP ${response.status} fetching ${url}`);
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
- } while (result.length);
112
- }
113
- export async function getTag(tag) {
114
- const response = await fetch(`https://api.github.com/repos/GMOD/jbrowse-components/releases/tags/${tag}`);
115
- if (response.ok) {
116
- const result = (await response.json());
117
- const file = result.assets?.find(f => f.name.includes('jbrowse-web'))?.browser_download_url;
118
- if (!file) {
119
- throw new Error('Could not find version specified. Use --listVersions to see all available versions');
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(`Could not find version: ${response.statusText}`);
300
+ throw new Error('Corrupt ZIP: no end-of-central-directory record');
124
301
  }
125
- export async function getBranch(branch) {
126
- return `https://s3.amazonaws.com/jbrowse.org/code/jb2/${branch}/jbrowse-web-${branch}.zip`;
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
- function wrapText(text, width, indent) {
129
- // Normalize: join single \n into spaces, preserve \n\n as paragraph breaks
130
- const normalized = text
131
- .replace(/\n\n/g, '\0')
132
- .replace(/\n/g, ' ')
133
- .replace(/\0/g, '\n\n');
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 line of normalized.split('\n')) {
136
- if (line.length <= width) {
137
- lines.push(line);
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
- const words = line.split(' ');
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.join(`\n${indent}`);
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(description);
163
- console.log(`\nUsage: ${usage || 'jbrowse <command> [options]'}`);
164
- console.log('\nOptions:');
165
- for (const [name, option] of Object.entries(options)) {
166
- const opt = option;
167
- const shortFlag = opt.short;
168
- const prefix = shortFlag ? ` -${shortFlag}, ` : ' ';
169
- const namePadded = `--${name}`.padEnd(22, ' ');
170
- const indent = ' '.repeat(prefix.length + namePadded.length + 1);
171
- const descWidth = termWidth - indent.length;
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 = '4.3.0';
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": "4.3.0",
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
- "command-exists": "^1.2.9",
34
- "cors": "^2.8.6",
35
- "decompress": "^4.2.1",
36
- "express": "^5.2.1",
37
- "ixixx": "^3.0.4",
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
- "prebuild": "pnpm clean",
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
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { main } from '../dist/index.js'
4
- main(process.argv.slice(2))