react-msaview-cli 5.0.6 → 5.0.13

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/package.json CHANGED
@@ -1,30 +1,38 @@
1
1
  {
2
2
  "name": "react-msaview-cli",
3
- "version": "5.0.6",
3
+ "version": "5.0.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/GMOD/JBrowseMSA.git",
9
+ "directory": "packages/cli"
10
+ },
6
11
  "bin": {
7
12
  "react-msaview-cli": "./dist/index.js"
8
13
  },
9
- "main": "dist/index.js",
10
- "types": "dist/index.d.ts",
11
14
  "exports": {
12
15
  ".": {
13
16
  "types": "./dist/index.d.ts",
14
17
  "default": "./dist/index.js"
15
18
  }
16
19
  },
20
+ "types": "dist/index.d.ts",
17
21
  "files": [
18
22
  "dist",
19
23
  "src"
20
24
  ],
25
+ "devDependencies": {
26
+ "@types/node": "^25.6.0"
27
+ },
21
28
  "dependencies": {
22
- "msa-parsers": "5.0.6"
29
+ "msa-parsers": "5.0.13"
23
30
  },
24
31
  "scripts": {
25
32
  "clean": "rimraf dist",
26
33
  "build": "tsc",
27
34
  "watch": "tsc --watch",
28
35
  "test": "vitest"
29
- }
36
+ },
37
+ "main": "dist/index.js"
30
38
  }
@@ -0,0 +1,103 @@
1
+ import { spawn } from 'node:child_process'
2
+ import * as fs from 'node:fs'
3
+ import * as os from 'node:os'
4
+ import path from 'node:path'
5
+
6
+ import { toFasta } from './util.ts'
7
+
8
+ import type { InterProScanResponse, InterProScanResults } from 'msa-parsers'
9
+
10
+ const INTERPROSCAN_IMAGE = 'interpro/interproscan:latest'
11
+
12
+ export async function runDockerInterProScan(
13
+ sequences: { id: string; seq: string }[],
14
+ programs: string[],
15
+ ): Promise<InterProScanResults[]> {
16
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'interproscan-'))
17
+ const inputFile = path.join(tmpDir, 'input.fasta')
18
+
19
+ try {
20
+ fs.writeFileSync(inputFile, toFasta(sequences), 'utf8')
21
+
22
+ console.log(
23
+ ` Running InterProScan via Docker on ${sequences.length} sequences...`,
24
+ )
25
+ console.log(` Image: ${INTERPROSCAN_IMAGE}`)
26
+
27
+ await new Promise<void>((resolve, reject) => {
28
+ const args = [
29
+ 'run',
30
+ '--rm',
31
+ '-v',
32
+ `${tmpDir}:/data`,
33
+ INTERPROSCAN_IMAGE,
34
+ '-i',
35
+ '/data/input.fasta',
36
+ '-o',
37
+ '/data/output.json',
38
+ '-f',
39
+ 'JSON',
40
+ '-appl',
41
+ programs.join(','),
42
+ ]
43
+
44
+ console.log(` docker ${args.join(' ')}`)
45
+
46
+ const proc = spawn('docker', args, {
47
+ stdio: ['ignore', 'pipe', 'pipe'],
48
+ })
49
+
50
+ proc.stdout.on('data', (data: Buffer) => {
51
+ const line = data.toString().trim()
52
+ if (line) {
53
+ console.log(` ${line}`)
54
+ }
55
+ })
56
+
57
+ let stderr = ''
58
+ proc.stderr.on('data', (data: Buffer) => {
59
+ stderr += data.toString()
60
+ const line = data.toString().trim()
61
+ if (line) {
62
+ console.log(` ${line}`)
63
+ }
64
+ })
65
+
66
+ proc.on('close', code => {
67
+ if (code === 0) {
68
+ resolve()
69
+ } else {
70
+ reject(
71
+ new Error(
72
+ `Docker InterProScan failed with code ${code}: ${stderr}`,
73
+ ),
74
+ )
75
+ }
76
+ })
77
+
78
+ proc.on('error', err => {
79
+ reject(
80
+ new Error(
81
+ `Failed to run Docker: ${err.message}. Is Docker installed and running?`,
82
+ ),
83
+ )
84
+ })
85
+ })
86
+
87
+ const outputFile = path.join(tmpDir, 'output.json')
88
+ if (!fs.existsSync(outputFile)) {
89
+ throw new Error('InterProScan did not produce output file')
90
+ }
91
+
92
+ const outputContent = fs.readFileSync(outputFile, 'utf8')
93
+ const response: InterProScanResponse = JSON.parse(outputContent)
94
+
95
+ return response.results
96
+ } finally {
97
+ try {
98
+ fs.rmSync(tmpDir, { recursive: true })
99
+ } catch {
100
+ // ignore cleanup errors
101
+ }
102
+ }
103
+ }
package/src/ebi-api.ts CHANGED
@@ -1,18 +1,12 @@
1
- import type { InterProScanResults } from 'msa-parsers'
1
+ import type { InterProScanResponse, InterProScanResults } from 'msa-parsers'
2
2
 
3
3
  const BASE_URL = 'https://www.ebi.ac.uk/Tools/services/rest/iprscan5'
4
4
 
5
- interface InterProScanResponse {
6
- results: InterProScanResults[]
7
- }
8
-
9
5
  async function submitJob(
10
- sequences: { id: string; seq: string }[],
6
+ sequence: { id: string; seq: string },
11
7
  programs: string[],
12
8
  email: string,
13
9
  ): Promise<string> {
14
- const fastaSeq = sequences.map(s => `>${s.id}\n${s.seq}`).join('\n')
15
-
16
10
  const response = await fetch(`${BASE_URL}/run`, {
17
11
  method: 'POST',
18
12
  headers: {
@@ -20,13 +14,14 @@ async function submitJob(
20
14
  },
21
15
  body: new URLSearchParams({
22
16
  email,
23
- sequence: fastaSeq,
17
+ sequence: `>${sequence.id}\n${sequence.seq}`,
24
18
  appl: programs.join(','),
25
19
  }),
26
20
  })
27
21
 
28
22
  if (!response.ok) {
29
- throw new Error(`Failed to submit job: ${response.statusText}`)
23
+ const text = await response.text()
24
+ throw new Error(`Failed to submit job: ${response.statusText} - ${text}`)
30
25
  }
31
26
 
32
27
  return response.text()
@@ -49,9 +44,8 @@ async function getResults(jobId: string): Promise<InterProScanResponse> {
49
44
  }
50
45
 
51
46
  async function waitForJob(jobId: string): Promise<void> {
52
- console.log(` Waiting for job ${jobId}...`)
53
47
  let attempts = 0
54
- const maxAttempts = 300 // 5 minutes max wait
48
+ const maxAttempts = 300
55
49
 
56
50
  while (attempts < maxAttempts) {
57
51
  const status = await checkStatus(jobId)
@@ -78,25 +72,16 @@ export async function runEbiInterProScan(
78
72
  sequences: { id: string; seq: string }[],
79
73
  programs: string[],
80
74
  email: string,
81
- batchSize: number,
75
+ _batchSize: number,
82
76
  ): Promise<InterProScanResults[]> {
83
77
  const allResults: InterProScanResults[] = []
84
- const batches: { id: string; seq: string }[][] = []
85
-
86
- for (let i = 0; i < sequences.length; i += batchSize) {
87
- batches.push(sequences.slice(i, i + batchSize))
88
- }
89
-
90
- console.log(` Submitting ${batches.length} batch(es)...`)
91
78
 
92
- for (let i = 0; i < batches.length; i++) {
93
- const batch = batches[i]!
94
- console.log(
95
- ` Processing batch ${i + 1}/${batches.length} (${batch.length} sequences)...`,
96
- )
79
+ for (let i = 0; i < sequences.length; i++) {
80
+ const seq = sequences[i]!
81
+ console.log(` [${i + 1}/${sequences.length}] Submitting ${seq.id}...`)
97
82
 
98
- const jobId = await submitJob(batch, programs, email)
99
- console.log(` Job submitted: ${jobId}`)
83
+ const jobId = await submitJob(seq, programs, email)
84
+ console.log(` Job: ${jobId}`)
100
85
 
101
86
  await waitForJob(jobId)
102
87
 
@@ -105,7 +90,7 @@ export async function runEbiInterProScan(
105
90
  allResults.push(r)
106
91
  }
107
92
 
108
- console.log(` Batch ${i + 1} complete`)
93
+ console.log(` [${i + 1}/${sequences.length}] Done`)
109
94
  }
110
95
 
111
96
  return allResults
package/src/index.ts CHANGED
@@ -16,13 +16,25 @@ const { values, positionals } = parseArgs({
16
16
  type: 'boolean',
17
17
  default: false,
18
18
  },
19
+ docker: {
20
+ type: 'boolean',
21
+ default: false,
22
+ },
23
+ singularity: {
24
+ type: 'boolean',
25
+ default: false,
26
+ },
27
+ 'singularity-image': {
28
+ type: 'string',
29
+ default: 'docker://interpro/interproscan:latest',
30
+ },
19
31
  'interproscan-path': {
20
32
  type: 'string',
21
33
  default: 'interproscan.sh',
22
34
  },
23
35
  programs: {
24
36
  type: 'string',
25
- default: 'Pfam',
37
+ default: 'PfamA,CDD',
26
38
  },
27
39
  email: {
28
40
  type: 'string',
@@ -53,21 +65,30 @@ COMMANDS:
53
65
  OPTIONS:
54
66
  -o, --output <file> Output GFF file (default: domains.gff)
55
67
  --local Use local InterProScan installation
68
+ --docker Use Docker (interpro/interproscan image)
69
+ --singularity Use Singularity/Apptainer container
70
+ --singularity-image <image> Singularity image (default: docker://interpro/interproscan:latest)
56
71
  --interproscan-path <path> Path to interproscan.sh (default: interproscan.sh)
57
- --programs <list> Comma-separated list of programs (default: Pfam)
72
+ --programs <list> Comma-separated list of programs (default: PfamA,CDD)
58
73
  --email <email> Email for EBI API (default: user@example.com)
59
74
  --batch-size <n> Number of sequences per API batch (default: 30)
60
75
  -h, --help Show this help message
61
76
 
62
77
  EXAMPLES:
63
- # Run InterProScan using EBI API
78
+ # Run InterProScan using EBI API (one sequence at a time)
64
79
  react-msaview-cli interproscan alignment.fasta -o domains.gff
65
80
 
66
- # Run with local InterProScan
81
+ # Run with Docker (processes all sequences locally, much faster)
82
+ react-msaview-cli interproscan alignment.fasta -o domains.gff --docker
83
+
84
+ # Run with local InterProScan installation
67
85
  react-msaview-cli interproscan alignment.fasta -o domains.gff --local
68
86
 
69
- # Specify programs
70
- react-msaview-cli interproscan alignment.clustal -o domains.gff --programs Pfam,SMART
87
+ # Run with Singularity using a local .sif file
88
+ react-msaview-cli interproscan alignment.fasta -o domains.gff --singularity --singularity-image /path/to/interproscan.sif
89
+
90
+ # Run with Singularity pulling from Docker Hub (requires network)
91
+ react-msaview-cli interproscan alignment.fasta -o domains.gff --singularity
71
92
  `)
72
93
  }
73
94
 
@@ -90,6 +111,9 @@ async function main() {
90
111
  inputFile,
91
112
  outputFile: values.output,
92
113
  useLocal: values.local,
114
+ useDocker: values.docker,
115
+ useSingularity: values.singularity,
116
+ singularityImage: values['singularity-image'],
93
117
  interproscanPath: values['interproscan-path'],
94
118
  programs: values.programs.split(','),
95
119
  email: values.email,
@@ -6,8 +6,10 @@ import {
6
6
  parseMSA,
7
7
  } from 'msa-parsers'
8
8
 
9
+ import { runDockerInterProScan } from './docker-runner'
9
10
  import { runEbiInterProScan } from './ebi-api'
10
11
  import { runLocalInterProScan } from './local-runner'
12
+ import { runSingularityInterProScan } from './singularity-runner'
11
13
 
12
14
  import type { InterProScanResults } from 'msa-parsers'
13
15
 
@@ -15,6 +17,9 @@ export interface InterProScanOptions {
15
17
  inputFile: string
16
18
  outputFile: string
17
19
  useLocal: boolean
20
+ useDocker: boolean
21
+ useSingularity: boolean
22
+ singularityImage: string
18
23
  interproscanPath: string
19
24
  programs: string[]
20
25
  email: string
@@ -26,6 +31,9 @@ export async function runInterProScan(options: InterProScanOptions) {
26
31
  inputFile,
27
32
  outputFile,
28
33
  useLocal,
34
+ useDocker,
35
+ useSingularity,
36
+ singularityImage,
29
37
  interproscanPath,
30
38
  programs,
31
39
  email,
@@ -52,7 +60,17 @@ export async function runInterProScan(options: InterProScanOptions) {
52
60
 
53
61
  let allResults: InterProScanResults[]
54
62
 
55
- if (useLocal) {
63
+ if (useSingularity) {
64
+ console.log('Running InterProScan via Singularity...')
65
+ allResults = await runSingularityInterProScan(
66
+ sequences,
67
+ programs,
68
+ singularityImage,
69
+ )
70
+ } else if (useDocker) {
71
+ console.log('Running InterProScan via Docker...')
72
+ allResults = await runDockerInterProScan(sequences, programs)
73
+ } else if (useLocal) {
56
74
  console.log(`Running local InterProScan at ${interproscanPath}...`)
57
75
  allResults = await runLocalInterProScan(
58
76
  sequences,
@@ -60,11 +78,11 @@ export async function runInterProScan(options: InterProScanOptions) {
60
78
  programs,
61
79
  )
62
80
  } else {
63
- console.log(`Running InterProScan via EBI API...`)
81
+ console.log('Running InterProScan via EBI API...')
64
82
  allResults = await runEbiInterProScan(sequences, programs, email, batchSize)
65
83
  }
66
84
 
67
- console.log(`Converting results to GFF...`)
85
+ console.log('Converting results to GFF...')
68
86
  const gff = interProResponseToGFF(allResults)
69
87
 
70
88
  console.log(`Writing output to ${outputFile}...`)
@@ -3,11 +3,9 @@ import * as fs from 'node:fs'
3
3
  import * as os from 'node:os'
4
4
  import path from 'node:path'
5
5
 
6
- import type { InterProScanResults } from 'msa-parsers'
6
+ import { toFasta } from './util.ts'
7
7
 
8
- interface InterProScanResponse {
9
- results: InterProScanResults[]
10
- }
8
+ import type { InterProScanResponse, InterProScanResults } from 'msa-parsers'
11
9
 
12
10
  export async function runLocalInterProScan(
13
11
  sequences: { id: string; seq: string }[],
@@ -19,9 +17,7 @@ export async function runLocalInterProScan(
19
17
  const outputFile = path.join(tmpDir, 'output.json')
20
18
 
21
19
  try {
22
- const fastaContent = sequences.map(s => `>${s.id}\n${s.seq}`).join('\n')
23
-
24
- fs.writeFileSync(inputFile, fastaContent, 'utf8')
20
+ fs.writeFileSync(inputFile, toFasta(sequences), 'utf8')
25
21
 
26
22
  console.log(` Running InterProScan on ${sequences.length} sequences...`)
27
23
 
@@ -0,0 +1,104 @@
1
+ import { spawn } from 'node:child_process'
2
+ import * as fs from 'node:fs'
3
+ import * as os from 'node:os'
4
+ import path from 'node:path'
5
+
6
+ import { toFasta } from './util.ts'
7
+
8
+ import type { InterProScanResponse, InterProScanResults } from 'msa-parsers'
9
+
10
+ const DEFAULT_SINGULARITY_IMAGE = 'docker://interpro/interproscan:latest'
11
+
12
+ export async function runSingularityInterProScan(
13
+ sequences: { id: string; seq: string }[],
14
+ programs: string[],
15
+ imagePath = DEFAULT_SINGULARITY_IMAGE,
16
+ ): Promise<InterProScanResults[]> {
17
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'interproscan-'))
18
+ const inputFile = path.join(tmpDir, 'input.fasta')
19
+
20
+ try {
21
+ fs.writeFileSync(inputFile, toFasta(sequences), 'utf8')
22
+
23
+ console.log(
24
+ ` Running InterProScan via Singularity on ${sequences.length} sequences...`,
25
+ )
26
+ console.log(` Image: ${imagePath}`)
27
+
28
+ await new Promise<void>((resolve, reject) => {
29
+ const args = [
30
+ 'exec',
31
+ '--bind',
32
+ `${tmpDir}:/data`,
33
+ imagePath,
34
+ '/opt/interproscan/interproscan.sh',
35
+ '-i',
36
+ '/data/input.fasta',
37
+ '-o',
38
+ '/data/output.json',
39
+ '-f',
40
+ 'JSON',
41
+ '-appl',
42
+ programs.join(','),
43
+ ]
44
+
45
+ console.log(` singularity ${args.join(' ')}`)
46
+
47
+ const proc = spawn('singularity', args, {
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ })
50
+
51
+ proc.stdout.on('data', (data: Buffer) => {
52
+ const line = data.toString().trim()
53
+ if (line) {
54
+ console.log(` ${line}`)
55
+ }
56
+ })
57
+
58
+ let stderr = ''
59
+ proc.stderr.on('data', (data: Buffer) => {
60
+ stderr += data.toString()
61
+ const line = data.toString().trim()
62
+ if (line) {
63
+ console.log(` ${line}`)
64
+ }
65
+ })
66
+
67
+ proc.on('close', code => {
68
+ if (code === 0) {
69
+ resolve()
70
+ } else {
71
+ reject(
72
+ new Error(
73
+ `Singularity InterProScan failed with code ${code}: ${stderr}`,
74
+ ),
75
+ )
76
+ }
77
+ })
78
+
79
+ proc.on('error', err => {
80
+ reject(
81
+ new Error(
82
+ `Failed to run Singularity: ${err.message}. Is Singularity/Apptainer installed?`,
83
+ ),
84
+ )
85
+ })
86
+ })
87
+
88
+ const outputFile = path.join(tmpDir, 'output.json')
89
+ if (!fs.existsSync(outputFile)) {
90
+ throw new Error('InterProScan did not produce output file')
91
+ }
92
+
93
+ const outputContent = fs.readFileSync(outputFile, 'utf8')
94
+ const response: InterProScanResponse = JSON.parse(outputContent)
95
+
96
+ return response.results
97
+ } finally {
98
+ try {
99
+ fs.rmSync(tmpDir, { recursive: true })
100
+ } catch {
101
+ // ignore cleanup errors
102
+ }
103
+ }
104
+ }
package/src/util.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function toFasta(sequences: { id: string; seq: string }[]) {
2
+ return sequences.map(s => `>${s.id}\n${s.seq}`).join('\n')
3
+ }