@larsgw/formica 0.9.2 → 0.10.0

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 (50) hide show
  1. package/lib/bin/process-resources-index.js +0 -0
  2. package/lib/bin/process-resources.d.ts +6 -1
  3. package/lib/bin/process-resources.js +116 -25
  4. package/lib/bin/validate-catalog.js +0 -0
  5. package/lib/bin/validate-resources-text.js +3 -1
  6. package/lib/resources/parse-text.js +6 -3
  7. package/lib/taxon-names/index.js +1 -1
  8. package/package.json +15 -13
  9. package/.gitattributes +0 -1
  10. package/.github/workflows/ci.yml +0 -27
  11. package/CHANGELOG.md +0 -390
  12. package/eslint.config.js +0 -34
  13. package/lib/bin/SHEETS.js +0 -0
  14. package/lib/bin/clean-links.d.ts +0 -2
  15. package/lib/bin/clean-links.js +0 -170
  16. package/lib/bin/download-place-shapes.js +0 -188
  17. package/lib/bin/index-place-shapes.js +0 -115
  18. package/lib/bin/process-resources-problems.js +0 -177
  19. package/lib/bin/validate-linked-data.js +0 -0
  20. package/lib/resources/content/clavis.js +0 -10
  21. package/lib/resources/content/index.js +0 -0
  22. package/lib/resources/content/sdd.js +0 -151
  23. package/lib/resources/sdd.js +0 -78
  24. package/src/bin/generate-linked-data.ts +0 -762
  25. package/src/bin/process-resources-index.ts +0 -100
  26. package/src/bin/process-resources.ts +0 -510
  27. package/src/bin/util.ts +0 -74
  28. package/src/bin/validate-catalog.ts +0 -122
  29. package/src/bin/validate-resources-text.ts +0 -25
  30. package/src/catalog/entities.ts +0 -62
  31. package/src/catalog/entity.ts +0 -116
  32. package/src/catalog/index.ts +0 -33
  33. package/src/catalog/tables/author.ts +0 -15
  34. package/src/catalog/tables/index.ts +0 -14
  35. package/src/catalog/tables/place.ts +0 -14
  36. package/src/catalog/tables/publisher.ts +0 -15
  37. package/src/catalog/tables/taxon.ts +0 -17
  38. package/src/catalog/tables/work.ts +0 -65
  39. package/src/catalog/value.ts +0 -51
  40. package/src/csv.ts +0 -33
  41. package/src/index.ts +0 -4
  42. package/src/module.d.ts +0 -148
  43. package/src/resources/diff-resource.ts +0 -226
  44. package/src/resources/index.ts +0 -4
  45. package/src/resources/parse-name.ts +0 -392
  46. package/src/resources/parse-text.ts +0 -408
  47. package/src/resources/resource.ts +0 -10
  48. package/src/taxon-names/index.ts +0 -79
  49. package/test/resources.js +0 -374
  50. package/tsconfig.json +0 -15
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { promises as fs, existsSync as fileExists } from 'fs'
4
- import * as path from 'path'
5
-
6
- import { csv, resources } from '../index'
7
- import { numericSort } from './util'
8
-
9
- interface AmendedResourceMetadata extends ResourceMetadata {
10
- id: ResourceId,
11
- taxonCount: number
12
- }
13
-
14
- type SortObjectCallback = (a: string, b: string) => number
15
- function alphabeticSort (a: string, b: string): number {
16
- return a > b ? 1 : a < b ? -1 : 0
17
- }
18
-
19
- function sortObject (object: Record<string, unknown>, sorter?: SortObjectCallback): Record<string, unknown> {
20
- const sorted: Record<string, unknown> = {}
21
- for (const key of Object.keys(object).sort(sorter ?? numericSort)) {
22
- sorted[key] = object[key]
23
- }
24
- return sorted
25
- }
26
-
27
- function addTaxon (index: Record<string, TaxonId[]>, id: string, taxon: string[]) {
28
- if (!(id in index)) {
29
- index[id] = []
30
- }
31
- index[id].push(taxon[0])
32
- index[id].sort(numericSort)
33
- }
34
-
35
- async function main (args: string[]): Promise<void> {
36
- const REPO_ROOT = path.resolve(args[0])
37
-
38
- const files = await fs.readdir(path.join(REPO_ROOT, 'txt'))
39
-
40
- const gbifIndex: Record<string, TaxonId[]> = {}
41
- const colIndex: Record<string, TaxonId[]> = {}
42
- const resourceIndex: Record<ResourceId, AmendedResourceMetadata> = {}
43
-
44
- await Promise.all(files.map(async function (fileName) {
45
- if (!fileName.endsWith('.txt')) { return }
46
- const id = fileName.slice(0, -4)
47
- const file = await fs.readFile(path.join(REPO_ROOT, 'txt', fileName), 'utf-8')
48
-
49
- return Promise.all(resources.parseTextFileHeader(file).map(async function (resource, index) {
50
- const amendedResource = {
51
- ...resource,
52
- id: `${id}:${index + 1}`,
53
- taxonCount: 0
54
- }
55
-
56
- const dwcFile = path.join(REPO_ROOT, 'dwc', `${id}-${index + 1}.csv`)
57
- if (!fileExists(dwcFile)) {
58
- return
59
- }
60
-
61
- const [header, ...dwc] = csv.parseCsv(await fs.readFile(dwcFile, 'utf-8'))
62
- const gbifColumn = header.indexOf('gbifTaxonID')
63
- const gbifAcceptedColumn = header.indexOf('gbifAcceptedTaxonID')
64
- const colColumn = header.indexOf('colTaxonID')
65
- const colAcceptedColumn = header.indexOf('colAcceptedTaxonID')
66
- for (const taxon of dwc) {
67
- const gbifId = taxon[gbifColumn]
68
- if (gbifId) {
69
- addTaxon(gbifIndex, gbifId, taxon)
70
- if (taxon[gbifAcceptedColumn] !== taxon[gbifColumn]) {
71
- addTaxon(gbifIndex, taxon[gbifAcceptedColumn], taxon)
72
- }
73
- }
74
-
75
- const colId = taxon[colColumn]
76
- if (colId) {
77
- addTaxon(colIndex, colId, taxon)
78
- if (taxon[colAcceptedColumn] !== taxon[colColumn]) {
79
- addTaxon(colIndex, taxon[colAcceptedColumn], taxon)
80
- }
81
- }
82
-
83
- amendedResource.taxonCount += 1
84
- }
85
-
86
- resourceIndex[amendedResource.id] = amendedResource
87
- }))
88
- }))
89
-
90
- await Promise.all([
91
- fs.writeFile(path.join(REPO_ROOT, 'gbif.index.json'), JSON.stringify(sortObject(gbifIndex), null, 2)),
92
- fs.writeFile(path.join(REPO_ROOT, 'col.index.json'), JSON.stringify(sortObject(colIndex, alphabeticSort), null, 2)),
93
- fs.writeFile(path.join(REPO_ROOT, 'index.json'), JSON.stringify(sortObject(resourceIndex), null, 2))
94
- ])
95
- }
96
-
97
- main(process.argv.slice(2)).catch(error => {
98
- console.error(error)
99
- process.exit(1)
100
- })
@@ -1,510 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { promises as fs, existsSync as doesFileExist } from 'fs'
4
- import * as path from 'path'
5
- import { spawn } from 'child_process'
6
- import * as util from 'util'
7
-
8
- import { csv } from '../index'
9
- import { prompt, promptForAnswers, numericSort, runCommand } from './util'
10
-
11
- export enum ResourceProcessorSource {
12
- All = 'all',
13
- Unprocessed = 'unprocessed',
14
- Modified = 'modified'
15
- }
16
-
17
- const DWC_FIELDS: (keyof AmendedTaxon)[] = [
18
- 'scientificNameID',
19
- 'scientificName',
20
- 'scientificNameAuthorship',
21
- 'genericName',
22
- 'infragenericEpithet',
23
- 'specificEpithet',
24
- 'infraspecificEpithet',
25
-
26
- 'taxonRank',
27
- 'taxonRemarks',
28
- 'collectionCode',
29
-
30
- 'taxonomicStatus',
31
- 'acceptedNameUsageID',
32
- 'acceptedNameUsage',
33
-
34
- 'parentNameUsageID',
35
- 'parentNameUsage',
36
- 'kingdom',
37
- 'phylum',
38
- 'class',
39
- 'order',
40
- 'family',
41
- 'subfamily',
42
- 'genus',
43
- 'subgenus',
44
- 'higherClassification',
45
- 'verbatimIdentification',
46
-
47
- 'dynamicProperties',
48
-
49
- 'colTaxonID',
50
- 'gbifTaxonID',
51
- 'colAcceptedTaxonID',
52
- 'gbifAcceptedTaxonID'
53
- ]
54
-
55
- const DISPLAY_FIELDS: (keyof AmendedTaxon)[] = [
56
- 'scientificNameID',
57
- 'taxonRank',
58
- 'scientificName',
59
- 'taxonomicStatus',
60
- 'taxonRemarks',
61
- 'colTaxonID',
62
- 'gbifTaxonID'
63
- ]
64
-
65
- const GBIF_RANKS: Rank[] = [
66
- 'kingdom',
67
- 'phyllum',
68
- 'class',
69
- 'order',
70
- 'family',
71
- 'genus',
72
- 'species',
73
- 'subspecies',
74
- 'variety'
75
- ]
76
-
77
- function runGnverifier (names: string): Promise<string> {
78
- return new Promise((resolve, reject) => {
79
- const proc = spawn('gnverifier', ['-s', '1,11', '-f', 'compact', '-M'])
80
- let stdout = ''
81
- proc.stdout.on('data', data => { stdout += data })
82
- proc.stderr.pipe(process.stdout)
83
- proc.on('close', code => {
84
- if (code === 0) {
85
- resolve(stdout)
86
- } else {
87
- reject()
88
- }
89
- })
90
- proc.stdin.write(names)
91
- proc.stdin.end()
92
- })
93
- }
94
-
95
- async function listFiles (directory: string): Promise<string[]> {
96
- const input = await fs.readdir(directory)
97
- return input.map(file => path.basename(file, '.txt')).sort(numericSort)
98
- }
99
-
100
- async function listUnprocessedFiles (directory: string, outputDirectory: string): Promise<string[]> {
101
- const input = await listFiles(directory)
102
- const output = new Set(await fs.readdir(outputDirectory))
103
- return input.filter(file => !output.has(file + '-1.csv'))
104
- }
105
-
106
- async function listChangedFiles (directory: string): Promise<string[]> {
107
- const output = await runCommand('git', ['diff', '--name-only', 'HEAD', '--', directory], {
108
- cwd: directory
109
- })
110
- return output.trimEnd().split('\n').map(file => path.basename(file, '.txt')).sort(numericSort)
111
- }
112
-
113
- async function getOldFile (file: string): Promise<string> {
114
- const options = {
115
- cwd: path.dirname(file)
116
- }
117
- const gitRoot = (await runCommand('git', ['rev-parse', '--show-toplevel'], options)).trim()
118
- return await runCommand('git', ['show', 'HEAD:' + path.relative(gitRoot, file)], options)
119
- }
120
-
121
- class ResourceProcessor {
122
- DIR_ROOT: string;
123
- DIR_TXT: string;
124
- DIR_DWC: string;
125
- FILE_PROBLEMS: string;
126
-
127
- constructor (collectionPath: string) {
128
- this.DIR_ROOT = path.resolve(collectionPath)
129
- this.DIR_TXT = path.join(this.DIR_ROOT, 'txt')
130
- this.DIR_DWC = path.join(this.DIR_ROOT, 'dwc')
131
- this.FILE_PROBLEMS = path.join(this.DIR_ROOT, 'problems.csv')
132
- }
133
-
134
- async run (source: ResourceProcessorSource, config: ResourceProcessorConfig): Promise<void> {
135
- const ids = await this.listWorks(source)
136
- for (const id of ids) {
137
- await this.processWork(id, config)
138
- }
139
- }
140
-
141
- async listWorks (source: ResourceProcessorSource): Promise<string[]> {
142
- switch (source) {
143
- case ResourceProcessorSource.All:
144
- return listFiles(this.DIR_TXT)
145
- case ResourceProcessorSource.Unprocessed:
146
- return listUnprocessedFiles(this.DIR_TXT, this.DIR_DWC)
147
- case ResourceProcessorSource.Modified:
148
- return listChangedFiles(this.DIR_TXT)
149
- default:
150
- return []
151
- }
152
- }
153
-
154
- async processWork (id: WorkId, config: ResourceProcessorConfig): Promise<void> {
155
- const resources = await this.processResources(id, config)
156
-
157
- await Promise.all(resources.map(resource => {
158
- const header = DWC_FIELDS
159
- const table: string[][] = [header]
160
-
161
- for (const id in resource.taxa) {
162
- const taxon = resource.taxa[id] as unknown as Record<string, string | undefined>
163
- table.push(header.map(column => taxon[column] || ''))
164
- }
165
-
166
- return fs.writeFile(path.join(this.DIR_DWC, `${resource.file}.csv`), csv.formatCsv(table, ',').trim())
167
- }))
168
- }
169
-
170
- async processResources (id: WorkId, config: ResourceProcessorConfig): Promise<AmendedResource[]> {
171
- const resources = await this.processResourceText(id, config)
172
-
173
- const amendedResources = []
174
- for (const resource of resources) {
175
- const results = await this.processResourceDwc(resource, config)
176
-
177
- const skip = await this.shouldBeSkipped(resource.id)
178
-
179
- if (!skip) {
180
- const correct = this.checkResults(results)
181
- if (!correct) {
182
- const choice = await promptForAnswers(
183
- `${resource.workId}: problems found in ${resource.id}. Skip or retry (s/r)? `,
184
- ['s', 'S', 'r', 'R']
185
- )
186
-
187
- switch (choice) {
188
- case 's':
189
- case 'S': {
190
- const reason = await prompt('Reason for skipping? ')
191
- fs.appendFile(this.FILE_PROBLEMS, csv.formatCsv([[
192
- resource.workId,
193
- resource.id,
194
- reason
195
- ]]))
196
- console.log(`${resource.workId}: skipping ${resource.id}`)
197
- break
198
- }
199
-
200
- case 'r':
201
- case 'R': {
202
- console.log(`${resource.workId}: retrying ${resource.id}`)
203
- return this.processResources(id, config)
204
- }
205
- }
206
- }
207
- }
208
-
209
- amendedResources.push(results)
210
- }
211
-
212
- return amendedResources
213
- }
214
-
215
- async processResourceText (id: WorkId, config: ResourceProcessorConfig): Promise<Resource[]> {
216
- try {
217
- console.log(`${id}: generating Darwin Core`)
218
- const filePath = path.join(this.DIR_TXT, id + '.txt')
219
- const file = await fs.readFile(filePath, 'utf-8')
220
-
221
- let old = undefined
222
- if (config.update) {
223
- const dwc = []
224
- for (const file of await fs.readdir(this.DIR_DWC)) {
225
- if (file.startsWith(id + '-')) {
226
- const filePath = path.join(this.DIR_DWC, file)
227
- dwc.push(csv.parseCsv(await getOldFile(filePath)))
228
- }
229
- }
230
-
231
- old = { txt: await getOldFile(filePath), dwc }
232
- }
233
-
234
- const { resources } = await import('../index')
235
- return resources.parseTextFile(file, id, old)
236
- } catch (error) {
237
- console.log(error.message)
238
- await prompt(`${id}: generating Darwin Core failed, retry? `)
239
-
240
- // Clear cache to re-import
241
- const prefix = path.dirname(require.resolve('../index'))
242
- for (const file in require.cache) {
243
- if (file.startsWith(prefix)) {
244
- delete require.cache[file]
245
- }
246
- }
247
-
248
- return this.processResourceText(id, config)
249
- }
250
- }
251
-
252
- async processResourceDwc (resource: Resource, config: ResourceProcessorConfig): Promise<AmendedResource> {
253
- console.log(`${resource.workId}: matching ${resource.id}`)
254
-
255
- if (!config.updateMappings) {
256
- const file = path.join(this.DIR_DWC, resource.file + '.csv')
257
- if (doesFileExist(file)) {
258
- const [header, ...rows] = csv.parseCsv(await fs.readFile(file, 'utf-8'))
259
- for (const row of rows) {
260
- const oldTaxon = row.reduce((taxon, value, index) => {
261
- taxon[header[index]] = value
262
- return taxon
263
- }, {} as Record<string, string>)
264
- const taxon = resource.taxa[oldTaxon.scientificNameID] as AmendedTaxon
265
- if (taxon) {
266
- taxon.colTaxonID = oldTaxon.colTaxonID
267
- taxon.colAcceptedTaxonID = oldTaxon.colAcceptedTaxonID
268
- taxon.gbifTaxonID = oldTaxon.gbifTaxonID
269
- taxon.gbifAcceptedTaxonID = oldTaxon.gbifAcceptedTaxonID
270
- }
271
- }
272
- }
273
- return resource as AmendedResource
274
- }
275
-
276
- const filteredResults: Record<TaxonId, TaxonMatch[]> = {}
277
- const taxonNames: Record<string, TaxonId[]> = {}
278
- const names = new Set()
279
- for (const id in resource.taxa) {
280
- const name = resource.taxa[id].scientificName
281
-
282
- if (!taxonNames[name]) { taxonNames[name] = [] }
283
- taxonNames[name].push(id)
284
-
285
- names.add(name)
286
- filteredResults[id] = []
287
- }
288
-
289
- const result = await runGnverifier(Array.from(names).join('\n'))
290
- for (const results of result.trim().split('\n')) {
291
- interface MatchScoreDetails {
292
- cardinalityScore: number;
293
- }
294
-
295
- interface Match {
296
- currentRecordId: string;
297
- dataSourceId: number;
298
- matchedName: string;
299
- recordId: string;
300
- sortScore: number;
301
- isSynonym: boolean;
302
- classificationPath: string;
303
- classificationRanks: string;
304
- scoreDetails: MatchScoreDetails;
305
- }
306
-
307
- const { name, results: matches } = JSON.parse(results)
308
-
309
- if (!matches) {
310
- continue
311
- }
312
-
313
- // Fix author scoring for some species, see https://github.com/gnames/gnverifier/issues/129
314
- matches.sort((a: Match, b: Match) => {
315
- if (a.sortScore !== b.sortScore) {
316
- return b.sortScore - a.sortScore
317
- }
318
-
319
- return name === a.matchedName ? -1 : name === b.matchedName ? 1 : 0
320
- })
321
-
322
- for (const match of matches as Match[]) {
323
- const source = match.dataSourceId
324
- const currentRank = match.classificationRanks.split('|').pop()
325
-
326
- if (match.scoreDetails.cardinalityScore === 0) {
327
- // Rank mismatch
328
- continue
329
- } else if (source === 11 && currentRank === 'species' && match.classificationPath.endsWith(' spec')) {
330
- // GBIF species like "Nomada spec"
331
- continue
332
- }
333
-
334
- for (const loirId of taxonNames[name]) {
335
- const taxon = resource.taxa[loirId]
336
-
337
- if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
338
- // Exclude GBIF matches for ranks that are not in GBIF
339
- continue
340
- } else if (source === 11 && !match.isSynonym && currentRank !== taxon.taxonRank) {
341
- // Exclude matches with rank mismatches (only possible
342
- // for non-synonyms).
343
- continue
344
- }
345
-
346
- if (!filteredResults[loirId]) {
347
- filteredResults[loirId] = []
348
- }
349
-
350
- filteredResults[loirId].push({
351
- source,
352
- id: match.recordId,
353
- currentId: match.currentRecordId,
354
- classificationPath: match.classificationPath.split('|')
355
- })
356
- }
357
- }
358
- }
359
-
360
- const { taxonNames: { amendResource, groupNameMatches } } = await import('../index')
361
- const groupedNameMatches = groupNameMatches(filteredResults)
362
-
363
- const amendedResource: AmendedResource = { ...resource, taxa: { ...resource.taxa } }
364
- for (const source in groupedNameMatches) {
365
- const matches = await this.selectPrefixes(resource, groupedNameMatches, source)
366
- amendResource(amendedResource, source, matches)
367
- }
368
-
369
- return amendedResource
370
- }
371
-
372
- async selectPrefixes (resource: Resource, groupedNameMatches: GroupedNameMatches, source: string): Promise<Record<TaxonId, TaxonMatch>> {
373
- const prefixes = Object.keys(groupedNameMatches[source])
374
- if (prefixes.length === 0) {
375
- return {}
376
- } else if (prefixes.length === 1) {
377
- return groupedNameMatches[source][prefixes[0]]
378
- }
379
-
380
- // Count total mapped taxa
381
- const mappedTaxa: Record<TaxonId, boolean> = {}
382
- for (const prefix of prefixes) {
383
- for (const taxon in groupedNameMatches[source][prefix]) {
384
- mappedTaxa[taxon] = true
385
- }
386
- }
387
- const missedTaxonCount = Object.keys(mappedTaxa).length - Object.keys(groupedNameMatches[source][prefixes[0]]).length
388
-
389
- if (missedTaxonCount === 0) {
390
- // Multiple prefixes but the first one maps all taxa (not counting that are unmapped in all prefixes)
391
- return groupedNameMatches[source][prefixes[0]]
392
- }
393
-
394
- console.error(`${resource.workId}: source ${source} results in multiple prefixes`)
395
-
396
- let choice
397
- if (missedTaxonCount <= 5) {
398
- console.error(` Most common prefix misses ${missedTaxonCount} taxa: automatically selecting most common prefix...`)
399
- choice = '1'
400
- } else if (source === '1') {
401
- console.error(` Catalogue of Life: automatically selecting most common prefix...`)
402
- choice = '1'
403
- } else {
404
- for (let i = 0; i < prefixes.length; i++) {
405
- const prefix = prefixes[i]
406
- const taxa = groupedNameMatches[source][prefix]
407
- const taxonIds = Object.keys(taxa)
408
-
409
- console.error(` [${i + 1}] ${prefix} (${taxonIds.length} taxa)`)
410
- for (let j = 0; j < Math.min(9, taxonIds.length); j++) {
411
- const taxonId = taxonIds[j]
412
- const taxon = resource.taxa[taxonId]
413
- const match = taxa[taxonId]
414
- console.error(` taxon: ${taxonId} "${taxon.scientificName}" - ${match.classificationPath.join('|')}`)
415
- }
416
- if (taxonIds.length > 9) {
417
- console.error(` ...`)
418
- }
419
- }
420
-
421
- do {
422
- choice = await prompt(` Select prefixes (1-${prefixes.length})? `)
423
- } while (!/^(|\d+(,\d+)*)$/.test(choice))
424
- }
425
-
426
- console.error(` Applying selection...`)
427
-
428
- if (choice === '') {
429
- return {}
430
- }
431
-
432
- const matches: Record<TaxonId, TaxonMatch> = {}
433
- for (const i of choice.split(',')) {
434
- const prefix = prefixes[parseInt(i) - 1]
435
- const taxa = groupedNameMatches[source][prefix]
436
- for (const id in taxa) {
437
- if (id in matches) {
438
- continue
439
- }
440
- matches[id] = taxa[id]
441
- }
442
- }
443
-
444
- return matches
445
- }
446
-
447
- checkResults (resource: AmendedResource): boolean {
448
- let correct = true
449
- const missing = []
450
-
451
- for (const id in resource.taxa) {
452
- const taxon = resource.taxa[id]
453
- if (taxon.taxonomicStatus !== 'accepted') { continue }
454
-
455
- const missingCol = false // !taxon.colTaxonID
456
- const missingGbif = GBIF_RANKS.includes(taxon.taxonRank) && !taxon.gbifTaxonID
457
-
458
- if (missingCol || missingGbif) {
459
- correct = false
460
- missing.push(taxon)
461
- }
462
- }
463
-
464
- if (missing.length) {
465
- console.table(missing, DISPLAY_FIELDS)
466
- }
467
-
468
- return correct
469
- }
470
-
471
- async shouldBeSkipped (id: ResourceId): Promise<boolean> {
472
- const problems = csv.parseCsv(await fs.readFile(this.FILE_PROBLEMS, 'utf8'))
473
- return problems.some(([_work, resource, _problem]) => resource === id)
474
- }
475
- }
476
-
477
- function main (): void {
478
- const args = util.parseArgs({
479
- options: {
480
- source: {
481
- type: 'string',
482
- short: 's',
483
- default: 'unprocessed'
484
- },
485
- 'keep-mappings': {
486
- type: 'boolean',
487
- short: 'k'
488
- }
489
- },
490
- allowPositionals: true
491
- })
492
-
493
- const processor = new ResourceProcessor(args.positionals[0])
494
- process.on('exit', () => {
495
- process.stdout.write('\n')
496
- })
497
-
498
- const source = args.values.source as ResourceProcessorSource
499
- const config: ResourceProcessorConfig = {
500
- update: source !== 'unprocessed',
501
- updateMappings: !args.values['keep-mappings']
502
- }
503
-
504
- processor.run(source, config).catch(error => {
505
- console.error(error)
506
- process.exit(1)
507
- })
508
- }
509
-
510
- main()
package/src/bin/util.ts DELETED
@@ -1,74 +0,0 @@
1
- import * as readline from 'readline'
2
- import { spawn } from 'child_process'
3
-
4
- /**
5
- * Comparison function to sort strings with numerical components.
6
- */
7
- export function numericSort (a: string, b: string): number {
8
- const as = a.split(/(\d+)/)
9
- const bs = b.split(/(\d+)/)
10
- for (let i = 0; i < Math.max(as.length, bs.length); i++) {
11
- const ai = as[i]
12
- const bi = bs[i]
13
-
14
- if (ai === bi) {
15
- continue
16
- } else if (!ai) {
17
- return 1
18
- } else if (!bi) {
19
- return -1
20
- } else if (i % 2) {
21
- return parseInt(ai) - parseInt(bi)
22
- } else {
23
- return ai > bi ? -1 : 1
24
- }
25
- }
26
- return 0
27
- }
28
-
29
- /**
30
- * Create a CLI prompt and return any answer.
31
- */
32
- export function prompt (question: string): Promise<string> {
33
- const rl = readline.createInterface({
34
- input: process.stdin,
35
- output: process.stdout
36
- })
37
- return new Promise(resolve => {
38
- rl.question(question, (answer: string) => {
39
- rl.close()
40
- resolve(answer)
41
- })
42
- })
43
- }
44
-
45
- /**
46
- * Create a CLI prompt and only accept certain answers.
47
- */
48
- export async function promptForAnswers (question: string, answers: string[]) {
49
- let answer
50
-
51
- do {
52
- answer = (await prompt(question))[0]
53
- } while (!answers.includes(answer))
54
-
55
- return answer
56
- }
57
-
58
- /**
59
- * Run a command and return stdout.
60
- */
61
- export function runCommand (command: string, args: string[], options?: Record<string, unknown>): Promise<string> {
62
- return new Promise((resolve, reject) => {
63
- const proc = spawn(command, args, options)
64
- let stdout = ''
65
- proc.stdout.on('data', data => { stdout += data })
66
- proc.on('close', code => {
67
- if (code === 0) {
68
- resolve(stdout)
69
- } else {
70
- reject()
71
- }
72
- })
73
- })
74
- }