react-msaview-cli 5.2.0 → 5.3.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.
package/README.md CHANGED
@@ -9,8 +9,8 @@ Uses [msa-parsers](../msa-parsers/) for file format support.
9
9
 
10
10
  ```bash
11
11
  # From the monorepo root
12
- yarn install
13
- yarn workspace @react-msaview/cli build
12
+ pnpm install
13
+ pnpm --filter @react-msaview/cli build
14
14
 
15
15
  # Or install globally (after publishing)
16
16
  npm install -g @react-msaview/cli
@@ -28,15 +28,22 @@ react-msaview-cli interproscan <input-msa> [options]
28
28
 
29
29
  #### Options
30
30
 
31
- | Option | Description | Default |
32
- | ---------------------------- | ------------------------------------------------------ | ------------------ |
33
- | `-o, --output <file>` | Output GFF file path | `domains.gff` |
34
- | `--local` | Use local InterProScan installation instead of EBI API | `false` |
35
- | `--interproscan-path <path>` | Path to local interproscan.sh | `interproscan.sh` |
36
- | `--programs <list>` | Comma-separated list of InterProScan programs | `Pfam` |
37
- | `--email <email>` | Email for EBI API (required for EBI API usage) | `user@example.com` |
38
- | `--batch-size <n>` | Number of sequences per API batch | `30` |
39
- | `-h, --help` | Show help message | |
31
+ | Option | Description | Default |
32
+ | ---------------------------- | ------------------------------------------------------ | --------------------------------------- |
33
+ | `-o, --output <file>` | Output GFF file path | `domains.gff` |
34
+ | `--local` | Use a local InterProScan installation instead of EBI | `false` |
35
+ | `--docker` | Run InterProScan via the `interpro/interproscan` image | `false` |
36
+ | `--singularity` | Run InterProScan via a Singularity/Apptainer container | `false` |
37
+ | `--singularity-image <img>` | Singularity image to use | `docker://interpro/interproscan:latest` |
38
+ | `--interproscan-path <path>` | Path to local interproscan.sh | `interproscan.sh` |
39
+ | `--programs <list>` | Comma-separated list of InterProScan programs | `PfamA,CDD` |
40
+ | `--email <email>` | Email for EBI API (used only for EBI API runs) | `user@example.com` |
41
+ | `-h, --help` | Show help message | |
42
+
43
+ By default (no backend flag) the CLI submits sequences to the EBI InterProScan
44
+ REST API one at a time. `--local`, `--docker`, and `--singularity` instead run
45
+ InterProScan on the whole alignment locally, which is much faster for large
46
+ datasets.
40
47
 
41
48
  #### Supported MSA Formats
42
49
 
@@ -52,7 +59,7 @@ The CLI automatically detects the input format:
52
59
 
53
60
  When using `--programs`, you can specify any combination of:
54
61
 
55
- - `Pfam` - Protein families (default)
62
+ - `PfamA` - Protein families (part of the `PfamA,CDD` default)
56
63
  - `SMART` - Simple Modular Architecture Research Tool
57
64
  - `SUPERFAMILY` - Structural and functional annotation
58
65
  - `Gene3D` - Structural domain assignments
@@ -78,11 +85,6 @@ react-msaview-cli interproscan alignment.fasta -o domains.gff --email your@email
78
85
  react-msaview-cli interproscan alignment.fasta -o domains.gff \
79
86
  --programs Pfam,SMART,Gene3D \
80
87
  --email your@email.com
81
-
82
- # Smaller batch size for large sequences
83
- react-msaview-cli interproscan large_proteins.fasta -o domains.gff \
84
- --batch-size 10 \
85
- --email your@email.com
86
88
  ```
87
89
 
88
90
  ### Using Local InterProScan
@@ -104,6 +106,31 @@ react-msaview-cli interproscan alignment.fasta -o domains.gff \
104
106
  --programs Pfam,SMART
105
107
  ```
106
108
 
109
+ ### Using Docker
110
+
111
+ No local InterProScan install needed — just Docker:
112
+
113
+ ```bash
114
+ react-msaview-cli interproscan alignment.fasta -o domains.gff --docker
115
+ ```
116
+
117
+ This mounts a temp directory into the `interpro/interproscan` container, runs
118
+ the scan on the whole alignment at once, and reads the JSON back out.
119
+
120
+ ### Using Singularity / Apptainer
121
+
122
+ On HPC clusters where Docker is unavailable:
123
+
124
+ ```bash
125
+ # Pull from Docker Hub (requires network)
126
+ react-msaview-cli interproscan alignment.fasta -o domains.gff --singularity
127
+
128
+ # Or use a pre-pulled .sif image
129
+ react-msaview-cli interproscan alignment.fasta -o domains.gff \
130
+ --singularity \
131
+ --singularity-image /path/to/interproscan.sif
132
+ ```
133
+
107
134
  ### Different Input Formats
108
135
 
109
136
  ```bash
@@ -117,9 +144,27 @@ react-msaview-cli interproscan PF00001.stockholm -o domains.gff
117
144
  react-msaview-cli interproscan colabfold.a3m -o domains.gff
118
145
  ```
119
146
 
147
+ ## Worked example
148
+
149
+ ```console
150
+ $ react-msaview-cli interproscan gpcrs.fasta -o domains.gff --docker
151
+ Reading MSA from gpcrs.fasta...
152
+ Found 4 sequences
153
+ Processing 4 non-empty sequences...
154
+ Running InterProScan via Docker...
155
+ Running InterProScan via Docker on 4 sequences (image: interpro/interproscan:latest)...
156
+ docker run --rm -v /tmp/interproscan-Xyz12:/data interpro/interproscan:latest -i /data/input.fasta -o /data/output.json -f JSON -appl PfamA,CDD
157
+ Converting results to GFF...
158
+ Writing output to domains.gff...
159
+ Done!
160
+ ```
161
+
120
162
  ## Output Format
121
163
 
122
- The output is standard GFF3 format compatible with react-msaview:
164
+ The output is standard GFF3, with one `protein_match` line per domain hit.
165
+ `start`/`end` are 1-based positions in the **ungapped** sequence (gaps are
166
+ stripped before scanning), and the attributes carry the signature accession,
167
+ name, and description:
123
168
 
124
169
  ```gff
125
170
  ##gff-version 3
@@ -132,10 +177,26 @@ seq2 InterProScan protein_match 5 120 . . . Name=PF00001;signature_desc=7tm_1;de
132
177
 
133
178
  After generating the GFF file, you can load it in react-msaview:
134
179
 
135
- 1. Open your MSA file in react-msaview
136
- 2. Go to **Menu > Open domains...**
137
- 3. Select the generated GFF file
138
- 4. Domains will appear as colored boxes on the alignment
180
+ - Open your MSA file in react-msaview
181
+ - Go to **Menu > Open domains...** and select the generated GFF file
182
+ - Domains appear as colored boxes on the alignment
183
+
184
+ In the React component, pass it inline as the `gff` prop (see the "Protein
185
+ domains" example in `packages/examples`):
186
+
187
+ ```jsx
188
+ <MSAViewer msa={msaText} gff={domainsGff} />
189
+ ```
190
+
191
+ From R, pass the file (or string) as the `gff` argument:
192
+
193
+ ```r
194
+ msaview(msa = "alignment.fasta", gff = "domains.gff")
195
+ ```
196
+
197
+ The domains render as labelled boxes over the matching rows:
198
+
199
+ ![InterProScan domains rendered over an alignment](../../docs/media/example-domains.svg)
139
200
 
140
201
  ## Troubleshooting
141
202
 
@@ -143,14 +204,14 @@ After generating the GFF file, you can load it in react-msaview:
143
204
 
144
205
  If you get timeout errors with the EBI API:
145
206
 
146
- - Reduce `--batch-size` (try 10-20 for large proteins)
147
- - Use `--local` with a local InterProScan installation
207
+ - Use `--local`, `--docker`, or `--singularity` to run InterProScan yourself
148
208
  - Check your internet connection
209
+ - For large datasets, a local/container backend is much faster than the API
149
210
 
150
211
  ### Local InterProScan Not Found
151
212
 
152
213
  ```
153
- Error: Failed to run InterProScan: spawn interproscan.sh ENOENT
214
+ Error: Failed to run Local: spawn interproscan.sh ENOENT. Is interproscan.sh installed and on PATH?
154
215
  ```
155
216
 
156
217
  Make sure InterProScan is installed and specify the full path:
@@ -169,9 +230,10 @@ Make sure InterProScan is installed and specify the full path:
169
230
 
170
231
  The EBI InterProScan API has usage limits:
171
232
 
172
- - Maximum ~30 sequences per request (configurable via `--batch-size`)
173
- - Requests are processed sequentially to avoid overwhelming the server
174
- - For large datasets (>100 sequences), consider using local InterProScan
233
+ - Sequences are submitted one at a time, sequentially, to avoid overwhelming the
234
+ server
235
+ - For large datasets (>100 sequences), use `--local`, `--docker`, or
236
+ `--singularity` instead
175
237
 
176
238
  ## License
177
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-msaview-cli",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,28 +11,30 @@
11
11
  "bin": {
12
12
  "react-msaview-cli": "./dist/index.js"
13
13
  },
14
- "exports": {
15
- ".": {
16
- "types": "./dist/index.d.ts",
17
- "default": "./dist/index.js"
18
- }
19
- },
20
- "types": "dist/index.d.ts",
21
14
  "files": [
22
- "dist",
23
- "src"
15
+ "dist"
24
16
  ],
25
17
  "devDependencies": {
26
- "@types/node": "^25.9.1"
18
+ "@types/jsdom": "^21.1.7",
19
+ "@types/node": "^25.9.2"
27
20
  },
28
21
  "dependencies": {
29
- "msa-parsers": "5.2.0"
22
+ "@emotion/react": "^11.14.0",
23
+ "@emotion/styled": "^11.14.1",
24
+ "@jbrowse/core": "^4.3.0",
25
+ "@mui/material": "^7.3.11",
26
+ "jsdom": "^26.1.0",
27
+ "mobx": "^6.15.4",
28
+ "mobx-react": "^9.2.2",
29
+ "react": "^19.2.7",
30
+ "react-dom": "^19.2.7",
31
+ "msa-parsers": "5.3.0",
32
+ "react-msaview": "5.3.0"
30
33
  },
31
34
  "scripts": {
32
- "clean": "rimraf dist",
35
+ "clean": "node --eval \"fs.rmSync('dist',{recursive:true,force:true})\" --input-type=module",
33
36
  "build": "tsc",
34
37
  "watch": "tsc --watch",
35
38
  "test": "vitest"
36
- },
37
- "main": "dist/index.js"
39
+ }
38
40
  }
@@ -1,103 +0,0 @@
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 DELETED
@@ -1,97 +0,0 @@
1
- import type { InterProScanResponse, InterProScanResults } from 'msa-parsers'
2
-
3
- const BASE_URL = 'https://www.ebi.ac.uk/Tools/services/rest/iprscan5'
4
-
5
- async function submitJob(
6
- sequence: { id: string; seq: string },
7
- programs: string[],
8
- email: string,
9
- ): Promise<string> {
10
- const response = await fetch(`${BASE_URL}/run`, {
11
- method: 'POST',
12
- headers: {
13
- 'Content-Type': 'application/x-www-form-urlencoded',
14
- },
15
- body: new URLSearchParams({
16
- email,
17
- sequence: `>${sequence.id}\n${sequence.seq}`,
18
- appl: programs.join(','),
19
- }),
20
- })
21
-
22
- if (!response.ok) {
23
- const text = await response.text()
24
- throw new Error(`Failed to submit job: ${response.statusText} - ${text}`)
25
- }
26
-
27
- return response.text()
28
- }
29
-
30
- async function checkStatus(jobId: string): Promise<string> {
31
- const response = await fetch(`${BASE_URL}/status/${jobId}`)
32
- if (!response.ok) {
33
- throw new Error(`Failed to check status: ${response.statusText}`)
34
- }
35
- return response.text()
36
- }
37
-
38
- async function getResults(jobId: string): Promise<InterProScanResponse> {
39
- const response = await fetch(`${BASE_URL}/result/${jobId}/json`)
40
- if (!response.ok) {
41
- throw new Error(`Failed to get results: ${response.statusText}`)
42
- }
43
- return response.json() as Promise<InterProScanResponse>
44
- }
45
-
46
- async function waitForJob(jobId: string): Promise<void> {
47
- let attempts = 0
48
- const maxAttempts = 300
49
-
50
- while (attempts < maxAttempts) {
51
- const status = await checkStatus(jobId)
52
-
53
- if (status.includes('FINISHED')) {
54
- return
55
- }
56
- if (status.includes('FAILURE') || status.includes('ERROR')) {
57
- throw new Error(`Job ${jobId} failed: ${status}`)
58
- }
59
-
60
- await new Promise(resolve => setTimeout(resolve, 1000))
61
- attempts++
62
-
63
- if (attempts % 10 === 0) {
64
- console.log(` Still waiting... (${attempts}s)`)
65
- }
66
- }
67
-
68
- throw new Error(`Timeout waiting for job ${jobId}`)
69
- }
70
-
71
- export async function runEbiInterProScan(
72
- sequences: { id: string; seq: string }[],
73
- programs: string[],
74
- email: string,
75
- _batchSize: number,
76
- ): Promise<InterProScanResults[]> {
77
- const allResults: InterProScanResults[] = []
78
-
79
- for (let i = 0; i < sequences.length; i++) {
80
- const seq = sequences[i]!
81
- console.log(` [${i + 1}/${sequences.length}] Submitting ${seq.id}...`)
82
-
83
- const jobId = await submitJob(seq, programs, email)
84
- console.log(` Job: ${jobId}`)
85
-
86
- await waitForJob(jobId)
87
-
88
- const results = await getResults(jobId)
89
- for (const r of results.results) {
90
- allResults.push(r)
91
- }
92
-
93
- console.log(` [${i + 1}/${sequences.length}] Done`)
94
- }
95
-
96
- return allResults
97
- }
package/src/index.ts DELETED
@@ -1,132 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { parseArgs } from 'node:util'
4
-
5
- import { runInterProScan } from './interproscan-msa'
6
-
7
- const { values, positionals } = parseArgs({
8
- allowPositionals: true,
9
- options: {
10
- output: {
11
- type: 'string',
12
- short: 'o',
13
- default: 'domains.gff',
14
- },
15
- local: {
16
- type: 'boolean',
17
- default: false,
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
- },
31
- 'interproscan-path': {
32
- type: 'string',
33
- default: 'interproscan.sh',
34
- },
35
- programs: {
36
- type: 'string',
37
- default: 'PfamA,CDD',
38
- },
39
- email: {
40
- type: 'string',
41
- default: 'user@example.com',
42
- },
43
- 'batch-size': {
44
- type: 'string',
45
- default: '30',
46
- },
47
- help: {
48
- type: 'boolean',
49
- short: 'h',
50
- default: false,
51
- },
52
- },
53
- })
54
-
55
- function printHelp() {
56
- console.log(`
57
- react-msaview-cli - CLI tools for react-msaview
58
-
59
- USAGE:
60
- react-msaview-cli interproscan <input-msa> [options]
61
-
62
- COMMANDS:
63
- interproscan Run InterProScan on all sequences in an MSA file
64
-
65
- OPTIONS:
66
- -o, --output <file> Output GFF file (default: domains.gff)
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)
71
- --interproscan-path <path> Path to interproscan.sh (default: interproscan.sh)
72
- --programs <list> Comma-separated list of programs (default: PfamA,CDD)
73
- --email <email> Email for EBI API (default: user@example.com)
74
- --batch-size <n> Number of sequences per API batch (default: 30)
75
- -h, --help Show this help message
76
-
77
- EXAMPLES:
78
- # Run InterProScan using EBI API (one sequence at a time)
79
- react-msaview-cli interproscan alignment.fasta -o domains.gff
80
-
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
85
- react-msaview-cli interproscan alignment.fasta -o domains.gff --local
86
-
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
92
- `)
93
- }
94
-
95
- async function main() {
96
- if (values.help || positionals.length === 0) {
97
- printHelp()
98
- process.exit(0)
99
- }
100
-
101
- const command = positionals[0]
102
-
103
- if (command === 'interproscan') {
104
- const inputFile = positionals[1]
105
- if (!inputFile) {
106
- console.error('Error: Input MSA file is required')
107
- process.exit(1)
108
- }
109
-
110
- await runInterProScan({
111
- inputFile,
112
- outputFile: values.output,
113
- useLocal: values.local,
114
- useDocker: values.docker,
115
- useSingularity: values.singularity,
116
- singularityImage: values['singularity-image'],
117
- interproscanPath: values['interproscan-path'],
118
- programs: values.programs.split(','),
119
- email: values.email,
120
- batchSize: Number.parseInt(values['batch-size'], 10),
121
- })
122
- } else {
123
- console.error(`Unknown command: ${command}`)
124
- printHelp()
125
- process.exit(1)
126
- }
127
- }
128
-
129
- main().catch((error: unknown) => {
130
- console.error('Error:', error instanceof Error ? error.message : error)
131
- process.exit(1)
132
- })
@@ -1,92 +0,0 @@
1
- import * as fs from 'node:fs'
2
-
3
- import {
4
- getUngappedSequence,
5
- interProResponseToGFF,
6
- parseMSA,
7
- } from 'msa-parsers'
8
-
9
- import { runDockerInterProScan } from './docker-runner'
10
- import { runEbiInterProScan } from './ebi-api'
11
- import { runLocalInterProScan } from './local-runner'
12
- import { runSingularityInterProScan } from './singularity-runner'
13
-
14
- import type { InterProScanResults } from 'msa-parsers'
15
-
16
- export interface InterProScanOptions {
17
- inputFile: string
18
- outputFile: string
19
- useLocal: boolean
20
- useDocker: boolean
21
- useSingularity: boolean
22
- singularityImage: string
23
- interproscanPath: string
24
- programs: string[]
25
- email: string
26
- batchSize: number
27
- }
28
-
29
- export async function runInterProScan(options: InterProScanOptions) {
30
- const {
31
- inputFile,
32
- outputFile,
33
- useLocal,
34
- useDocker,
35
- useSingularity,
36
- singularityImage,
37
- interproscanPath,
38
- programs,
39
- email,
40
- batchSize,
41
- } = options
42
-
43
- console.log(`Reading MSA from ${inputFile}...`)
44
- const msaText = fs.readFileSync(inputFile, 'utf8')
45
- const msa = parseMSA(msaText)
46
-
47
- const names = msa.getNames()
48
- console.log(`Found ${names.length} sequences`)
49
-
50
- const sequences: { id: string; seq: string }[] = []
51
- for (const name of names) {
52
- const alignedSeq = msa.getRow(name)
53
- const ungappedSeq = getUngappedSequence(alignedSeq)
54
- if (ungappedSeq.length > 0) {
55
- sequences.push({ id: name, seq: ungappedSeq })
56
- }
57
- }
58
-
59
- console.log(`Processing ${sequences.length} non-empty sequences...`)
60
-
61
- let allResults: InterProScanResults[]
62
-
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) {
74
- console.log(`Running local InterProScan at ${interproscanPath}...`)
75
- allResults = await runLocalInterProScan(
76
- sequences,
77
- interproscanPath,
78
- programs,
79
- )
80
- } else {
81
- console.log('Running InterProScan via EBI API...')
82
- allResults = await runEbiInterProScan(sequences, programs, email, batchSize)
83
- }
84
-
85
- console.log('Converting results to GFF...')
86
- const gff = interProResponseToGFF(allResults)
87
-
88
- console.log(`Writing output to ${outputFile}...`)
89
- fs.writeFileSync(outputFile, gff, 'utf8')
90
-
91
- console.log('Done!')
92
- }
@@ -1,83 +0,0 @@
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
- export async function runLocalInterProScan(
11
- sequences: { id: string; seq: string }[],
12
- interproscanPath: string,
13
- programs: string[],
14
- ): Promise<InterProScanResults[]> {
15
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'interproscan-'))
16
- const inputFile = path.join(tmpDir, 'input.fasta')
17
- const outputFile = path.join(tmpDir, 'output.json')
18
-
19
- try {
20
- fs.writeFileSync(inputFile, toFasta(sequences), 'utf8')
21
-
22
- console.log(` Running InterProScan on ${sequences.length} sequences...`)
23
-
24
- await new Promise<void>((resolve, reject) => {
25
- const args = [
26
- '-i',
27
- inputFile,
28
- '-o',
29
- outputFile,
30
- '-f',
31
- 'JSON',
32
- '-appl',
33
- programs.join(','),
34
- '--goterms',
35
- '--pathways',
36
- ]
37
-
38
- const proc = spawn(interproscanPath, args, {
39
- stdio: ['ignore', 'pipe', 'pipe'],
40
- })
41
-
42
- let stderr = ''
43
-
44
- proc.stdout.on('data', (data: Buffer) => {
45
- const line = data.toString().trim()
46
- if (line) {
47
- console.log(` ${line}`)
48
- }
49
- })
50
-
51
- proc.stderr.on('data', (data: Buffer) => {
52
- stderr += data.toString()
53
- })
54
-
55
- proc.on('close', code => {
56
- if (code === 0) {
57
- resolve()
58
- } else {
59
- reject(new Error(`InterProScan failed with code ${code}: ${stderr}`))
60
- }
61
- })
62
-
63
- proc.on('error', err => {
64
- reject(new Error(`Failed to run InterProScan: ${err.message}`))
65
- })
66
- })
67
-
68
- if (!fs.existsSync(outputFile)) {
69
- throw new Error('InterProScan did not produce output file')
70
- }
71
-
72
- const outputContent = fs.readFileSync(outputFile, 'utf8')
73
- const response: InterProScanResponse = JSON.parse(outputContent)
74
-
75
- return response.results
76
- } finally {
77
- try {
78
- fs.rmSync(tmpDir, { recursive: true })
79
- } catch {
80
- // ignore cleanup errors
81
- }
82
- }
83
- }
@@ -1,104 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- export function toFasta(sequences: { id: string; seq: string }[]) {
2
- return sequences.map(s => `>${s.id}\n${s.seq}`).join('\n')
3
- }