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 +90 -28
- package/package.json +17 -15
- package/src/docker-runner.ts +0 -103
- package/src/ebi-api.ts +0 -97
- package/src/index.ts +0 -132
- package/src/interproscan-msa.ts +0 -92
- package/src/local-runner.ts +0 -83
- package/src/singularity-runner.ts +0 -104
- package/src/util.ts +0 -3
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
|
-
|
|
13
|
-
|
|
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
|
|
35
|
-
| `--
|
|
36
|
-
| `--
|
|
37
|
-
| `--
|
|
38
|
-
| `--
|
|
39
|
-
|
|
|
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+

|
|
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
|
-
-
|
|
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
|
|
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
|
-
-
|
|
173
|
-
|
|
174
|
-
- For large datasets (>100 sequences),
|
|
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.
|
|
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/
|
|
18
|
+
"@types/jsdom": "^21.1.7",
|
|
19
|
+
"@types/node": "^25.9.2"
|
|
27
20
|
},
|
|
28
21
|
"dependencies": {
|
|
29
|
-
"
|
|
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": "
|
|
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
|
}
|
package/src/docker-runner.ts
DELETED
|
@@ -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
|
-
})
|
package/src/interproscan-msa.ts
DELETED
|
@@ -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
|
-
}
|
package/src/local-runner.ts
DELETED
|
@@ -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