@yangfei_93sky/biocli 0.2.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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/batch.d.ts +20 -0
- package/dist/batch.js +69 -0
- package/dist/build-manifest.d.ts +38 -0
- package/dist/build-manifest.js +186 -0
- package/dist/cache.d.ts +28 -0
- package/dist/cache.js +126 -0
- package/dist/cli-manifest.json +1500 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +336 -0
- package/dist/clis/_shared/common.d.ts +8 -0
- package/dist/clis/_shared/common.js +13 -0
- package/dist/clis/_shared/eutils.d.ts +9 -0
- package/dist/clis/_shared/eutils.js +9 -0
- package/dist/clis/_shared/organism-db.d.ts +23 -0
- package/dist/clis/_shared/organism-db.js +58 -0
- package/dist/clis/_shared/xml-helpers.d.ts +58 -0
- package/dist/clis/_shared/xml-helpers.js +266 -0
- package/dist/clis/aggregate/enrichment.d.ts +7 -0
- package/dist/clis/aggregate/enrichment.js +105 -0
- package/dist/clis/aggregate/gene-dossier.d.ts +13 -0
- package/dist/clis/aggregate/gene-dossier.js +248 -0
- package/dist/clis/aggregate/gene-profile.d.ts +16 -0
- package/dist/clis/aggregate/gene-profile.js +305 -0
- package/dist/clis/aggregate/literature-brief.d.ts +7 -0
- package/dist/clis/aggregate/literature-brief.js +79 -0
- package/dist/clis/aggregate/variant-dossier.d.ts +11 -0
- package/dist/clis/aggregate/variant-dossier.js +161 -0
- package/dist/clis/aggregate/variant-interpret.d.ts +10 -0
- package/dist/clis/aggregate/variant-interpret.js +210 -0
- package/dist/clis/aggregate/workflow-prepare.d.ts +12 -0
- package/dist/clis/aggregate/workflow-prepare.js +228 -0
- package/dist/clis/aggregate/workflow-scout.d.ts +13 -0
- package/dist/clis/aggregate/workflow-scout.js +175 -0
- package/dist/clis/clinvar/search.d.ts +8 -0
- package/dist/clis/clinvar/search.js +61 -0
- package/dist/clis/clinvar/variant.d.ts +7 -0
- package/dist/clis/clinvar/variant.js +53 -0
- package/dist/clis/enrichr/analyze.d.ts +7 -0
- package/dist/clis/enrichr/analyze.js +48 -0
- package/dist/clis/ensembl/lookup.d.ts +6 -0
- package/dist/clis/ensembl/lookup.js +38 -0
- package/dist/clis/ensembl/vep.d.ts +7 -0
- package/dist/clis/ensembl/vep.js +86 -0
- package/dist/clis/ensembl/xrefs.d.ts +6 -0
- package/dist/clis/ensembl/xrefs.js +36 -0
- package/dist/clis/gene/fetch.d.ts +10 -0
- package/dist/clis/gene/fetch.js +96 -0
- package/dist/clis/gene/info.d.ts +7 -0
- package/dist/clis/gene/info.js +37 -0
- package/dist/clis/gene/search.d.ts +7 -0
- package/dist/clis/gene/search.js +71 -0
- package/dist/clis/geo/dataset.d.ts +7 -0
- package/dist/clis/geo/dataset.js +55 -0
- package/dist/clis/geo/download.d.ts +17 -0
- package/dist/clis/geo/download.js +115 -0
- package/dist/clis/geo/samples.d.ts +7 -0
- package/dist/clis/geo/samples.js +57 -0
- package/dist/clis/geo/search.d.ts +8 -0
- package/dist/clis/geo/search.js +66 -0
- package/dist/clis/kegg/convert.d.ts +7 -0
- package/dist/clis/kegg/convert.js +37 -0
- package/dist/clis/kegg/disease.d.ts +6 -0
- package/dist/clis/kegg/disease.js +57 -0
- package/dist/clis/kegg/link.d.ts +7 -0
- package/dist/clis/kegg/link.js +36 -0
- package/dist/clis/kegg/pathway.d.ts +6 -0
- package/dist/clis/kegg/pathway.js +37 -0
- package/dist/clis/pubmed/abstract.d.ts +7 -0
- package/dist/clis/pubmed/abstract.js +42 -0
- package/dist/clis/pubmed/cited-by.d.ts +7 -0
- package/dist/clis/pubmed/cited-by.js +77 -0
- package/dist/clis/pubmed/fetch.d.ts +6 -0
- package/dist/clis/pubmed/fetch.js +36 -0
- package/dist/clis/pubmed/info.yaml +22 -0
- package/dist/clis/pubmed/related.d.ts +7 -0
- package/dist/clis/pubmed/related.js +81 -0
- package/dist/clis/pubmed/search.d.ts +8 -0
- package/dist/clis/pubmed/search.js +63 -0
- package/dist/clis/snp/lookup.d.ts +7 -0
- package/dist/clis/snp/lookup.js +57 -0
- package/dist/clis/sra/download.d.ts +18 -0
- package/dist/clis/sra/download.js +217 -0
- package/dist/clis/sra/run.d.ts +8 -0
- package/dist/clis/sra/run.js +77 -0
- package/dist/clis/sra/search.d.ts +8 -0
- package/dist/clis/sra/search.js +83 -0
- package/dist/clis/string/enrichment.d.ts +7 -0
- package/dist/clis/string/enrichment.js +50 -0
- package/dist/clis/string/network.d.ts +7 -0
- package/dist/clis/string/network.js +47 -0
- package/dist/clis/string/partners.d.ts +4 -0
- package/dist/clis/string/partners.js +44 -0
- package/dist/clis/taxonomy/lookup.d.ts +8 -0
- package/dist/clis/taxonomy/lookup.js +54 -0
- package/dist/clis/uniprot/fetch.d.ts +7 -0
- package/dist/clis/uniprot/fetch.js +82 -0
- package/dist/clis/uniprot/search.d.ts +6 -0
- package/dist/clis/uniprot/search.js +65 -0
- package/dist/clis/uniprot/sequence.d.ts +7 -0
- package/dist/clis/uniprot/sequence.js +51 -0
- package/dist/commander-adapter.d.ts +27 -0
- package/dist/commander-adapter.js +286 -0
- package/dist/completion.d.ts +19 -0
- package/dist/completion.js +117 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +94 -0
- package/dist/databases/enrichr.d.ts +28 -0
- package/dist/databases/enrichr.js +131 -0
- package/dist/databases/ensembl.d.ts +14 -0
- package/dist/databases/ensembl.js +106 -0
- package/dist/databases/index.d.ts +45 -0
- package/dist/databases/index.js +49 -0
- package/dist/databases/kegg.d.ts +26 -0
- package/dist/databases/kegg.js +136 -0
- package/dist/databases/ncbi.d.ts +28 -0
- package/dist/databases/ncbi.js +144 -0
- package/dist/databases/string-db.d.ts +19 -0
- package/dist/databases/string-db.js +105 -0
- package/dist/databases/uniprot.d.ts +13 -0
- package/dist/databases/uniprot.js +110 -0
- package/dist/discovery.d.ts +32 -0
- package/dist/discovery.js +235 -0
- package/dist/doctor.d.ts +19 -0
- package/dist/doctor.js +151 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.js +105 -0
- package/dist/execution.d.ts +15 -0
- package/dist/execution.js +178 -0
- package/dist/hooks.d.ts +48 -0
- package/dist/hooks.js +58 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +31 -0
- package/dist/ncbi-fetch.d.ts +10 -0
- package/dist/ncbi-fetch.js +10 -0
- package/dist/output.d.ts +18 -0
- package/dist/output.js +394 -0
- package/dist/pipeline/executor.d.ts +22 -0
- package/dist/pipeline/executor.js +40 -0
- package/dist/pipeline/index.d.ts +6 -0
- package/dist/pipeline/index.js +6 -0
- package/dist/pipeline/registry.d.ts +16 -0
- package/dist/pipeline/registry.js +31 -0
- package/dist/pipeline/steps/fetch.d.ts +21 -0
- package/dist/pipeline/steps/fetch.js +160 -0
- package/dist/pipeline/steps/transform.d.ts +26 -0
- package/dist/pipeline/steps/transform.js +92 -0
- package/dist/pipeline/steps/xml-parse.d.ts +12 -0
- package/dist/pipeline/steps/xml-parse.js +27 -0
- package/dist/pipeline/template.d.ts +35 -0
- package/dist/pipeline/template.js +312 -0
- package/dist/rate-limiter.d.ts +56 -0
- package/dist/rate-limiter.js +120 -0
- package/dist/registry-api.d.ts +15 -0
- package/dist/registry-api.js +13 -0
- package/dist/registry.d.ts +90 -0
- package/dist/registry.js +100 -0
- package/dist/schema.d.ts +80 -0
- package/dist/schema.js +72 -0
- package/dist/spinner.d.ts +19 -0
- package/dist/spinner.js +37 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +27 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.js +40 -0
- package/dist/validate.d.ts +29 -0
- package/dist/validate.js +136 -0
- package/dist/verify.d.ts +20 -0
- package/dist/verify.js +131 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +36 -0
- package/dist/xml-parser.d.ts +19 -0
- package/dist/xml-parser.js +119 -0
- package/dist/yaml-schema.d.ts +40 -0
- package/dist/yaml-schema.js +62 -0
- package/package.json +68 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STRING database backend for biocli.
|
|
3
|
+
*
|
|
4
|
+
* STRING API (https://string-db.org/api):
|
|
5
|
+
* - No authentication required (optional API key for batch jobs)
|
|
6
|
+
* - Rate limit: 1 request per second (documented)
|
|
7
|
+
* - Response format: JSON via /api/json/..., also supports TSV, XML, image
|
|
8
|
+
* - Multiple identifiers separated by %0d (newline-encoded)
|
|
9
|
+
* - species parameter: NCBI taxonomy ID (9606 for human)
|
|
10
|
+
*/
|
|
11
|
+
import { type DatabaseBackend } from './index.js';
|
|
12
|
+
/** Build a STRING API URL. Format is embedded in path: /api/{format}/{endpoint} */
|
|
13
|
+
export declare function buildStringUrl(endpoint: string, params?: Record<string, string>): string;
|
|
14
|
+
/**
|
|
15
|
+
* Encode multiple identifiers for STRING API.
|
|
16
|
+
* STRING uses %0d (URL-encoded newline) as separator.
|
|
17
|
+
*/
|
|
18
|
+
export declare function encodeStringIds(ids: string[]): string;
|
|
19
|
+
export declare const stringBackend: DatabaseBackend;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STRING database backend for biocli.
|
|
3
|
+
*
|
|
4
|
+
* STRING API (https://string-db.org/api):
|
|
5
|
+
* - No authentication required (optional API key for batch jobs)
|
|
6
|
+
* - Rate limit: 1 request per second (documented)
|
|
7
|
+
* - Response format: JSON via /api/json/..., also supports TSV, XML, image
|
|
8
|
+
* - Multiple identifiers separated by %0d (newline-encoded)
|
|
9
|
+
* - species parameter: NCBI taxonomy ID (9606 for human)
|
|
10
|
+
*/
|
|
11
|
+
import { getRateLimiterForDatabase } from '../rate-limiter.js';
|
|
12
|
+
import { ApiError } from '../errors.js';
|
|
13
|
+
import { sleep } from '../utils.js';
|
|
14
|
+
import { registerBackend } from './index.js';
|
|
15
|
+
const BASE_URL = 'https://string-db.org/api';
|
|
16
|
+
const MAX_RETRIES = 2;
|
|
17
|
+
const BASE_RETRY_DELAY_MS = 1000;
|
|
18
|
+
/** Build a STRING API URL. Format is embedded in path: /api/{format}/{endpoint} */
|
|
19
|
+
export function buildStringUrl(endpoint, params) {
|
|
20
|
+
const url = new URL(`${BASE_URL}/json/${endpoint}`);
|
|
21
|
+
if (params) {
|
|
22
|
+
for (const [k, v] of Object.entries(params)) {
|
|
23
|
+
if (v !== undefined && v !== '')
|
|
24
|
+
url.searchParams.set(k, v);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Identify ourselves
|
|
28
|
+
if (!url.searchParams.has('caller_identity')) {
|
|
29
|
+
url.searchParams.set('caller_identity', 'biocli');
|
|
30
|
+
}
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Encode multiple identifiers for STRING API.
|
|
35
|
+
* STRING uses %0d (URL-encoded newline) as separator.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeStringIds(ids) {
|
|
38
|
+
return ids.join('%0d');
|
|
39
|
+
}
|
|
40
|
+
/** Low-level STRING fetch with rate limiting and retry. */
|
|
41
|
+
async function stringFetch(url, opts) {
|
|
42
|
+
if (!opts?.skipRateLimit) {
|
|
43
|
+
const limiter = getRateLimiterForDatabase('string', 1);
|
|
44
|
+
await limiter.acquire();
|
|
45
|
+
}
|
|
46
|
+
let lastError;
|
|
47
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
method: opts?.method ?? 'GET',
|
|
51
|
+
headers: { 'Accept': 'application/json', ...opts?.headers },
|
|
52
|
+
body: opts?.body,
|
|
53
|
+
});
|
|
54
|
+
if (response.status === 429 || response.status === 503) {
|
|
55
|
+
if (attempt < MAX_RETRIES) {
|
|
56
|
+
await sleep(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new ApiError('STRING API rate limit exceeded. Try again in a few seconds.');
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new ApiError(`STRING API returned HTTP ${response.status}: ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
return response;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof ApiError)
|
|
68
|
+
throw err;
|
|
69
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
70
|
+
if (attempt < MAX_RETRIES) {
|
|
71
|
+
await sleep(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new ApiError(`STRING request failed after ${MAX_RETRIES + 1} attempts: ${lastError?.message ?? 'unknown error'}`);
|
|
77
|
+
}
|
|
78
|
+
function createContext() {
|
|
79
|
+
return {
|
|
80
|
+
databaseId: 'string',
|
|
81
|
+
async fetch(url, opts) {
|
|
82
|
+
return stringFetch(url, opts);
|
|
83
|
+
},
|
|
84
|
+
async fetchJson(url, opts) {
|
|
85
|
+
const response = await stringFetch(url, opts);
|
|
86
|
+
return response.json();
|
|
87
|
+
},
|
|
88
|
+
async fetchXml(url, opts) {
|
|
89
|
+
const response = await stringFetch(url, opts);
|
|
90
|
+
return response.text();
|
|
91
|
+
},
|
|
92
|
+
async fetchText(url, opts) {
|
|
93
|
+
const response = await stringFetch(url, opts);
|
|
94
|
+
return response.text();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export const stringBackend = {
|
|
99
|
+
id: 'string',
|
|
100
|
+
name: 'STRING',
|
|
101
|
+
baseUrl: BASE_URL,
|
|
102
|
+
rateLimit: 1,
|
|
103
|
+
createContext,
|
|
104
|
+
};
|
|
105
|
+
registerBackend(stringBackend);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UniProt database backend for biocli.
|
|
3
|
+
*
|
|
4
|
+
* UniProt REST API (https://rest.uniprot.org):
|
|
5
|
+
* - No authentication required
|
|
6
|
+
* - Rate limit: ~200 req/s (we cap at 50/s to be polite)
|
|
7
|
+
* - Response format: JSON (default), XML, TSV, FASTA
|
|
8
|
+
* - Pagination via Link headers
|
|
9
|
+
*/
|
|
10
|
+
import { type DatabaseBackend } from './index.js';
|
|
11
|
+
/** Build a UniProt API URL with query parameters. */
|
|
12
|
+
export declare function buildUniprotUrl(path: string, params?: Record<string, string>): string;
|
|
13
|
+
export declare const uniprotBackend: DatabaseBackend;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UniProt database backend for biocli.
|
|
3
|
+
*
|
|
4
|
+
* UniProt REST API (https://rest.uniprot.org):
|
|
5
|
+
* - No authentication required
|
|
6
|
+
* - Rate limit: ~200 req/s (we cap at 50/s to be polite)
|
|
7
|
+
* - Response format: JSON (default), XML, TSV, FASTA
|
|
8
|
+
* - Pagination via Link headers
|
|
9
|
+
*/
|
|
10
|
+
import { getRateLimiterForDatabase } from '../rate-limiter.js';
|
|
11
|
+
import { ApiError } from '../errors.js';
|
|
12
|
+
import { sleep } from '../utils.js';
|
|
13
|
+
import { registerBackend } from './index.js';
|
|
14
|
+
const BASE_URL = 'https://rest.uniprot.org';
|
|
15
|
+
const MAX_RETRIES = 2;
|
|
16
|
+
const BASE_RETRY_DELAY_MS = 500;
|
|
17
|
+
/** Build a UniProt API URL with query parameters. */
|
|
18
|
+
export function buildUniprotUrl(path, params) {
|
|
19
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
20
|
+
if (params) {
|
|
21
|
+
for (const [k, v] of Object.entries(params)) {
|
|
22
|
+
if (v !== undefined && v !== '')
|
|
23
|
+
url.searchParams.set(k, v);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return url.toString();
|
|
27
|
+
}
|
|
28
|
+
/** Low-level UniProt fetch with rate limiting and retry. */
|
|
29
|
+
async function uniprotFetch(url, opts) {
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
if (opts?.params) {
|
|
32
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
33
|
+
if (v !== undefined && v !== '')
|
|
34
|
+
parsed.searchParams.set(k, v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const finalUrl = parsed.toString();
|
|
38
|
+
if (!opts?.skipRateLimit) {
|
|
39
|
+
const limiter = getRateLimiterForDatabase('uniprot', 50);
|
|
40
|
+
await limiter.acquire();
|
|
41
|
+
}
|
|
42
|
+
let lastError;
|
|
43
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(finalUrl, {
|
|
46
|
+
method: opts?.method ?? 'GET',
|
|
47
|
+
headers: {
|
|
48
|
+
'Accept': 'application/json',
|
|
49
|
+
...opts?.headers,
|
|
50
|
+
},
|
|
51
|
+
body: opts?.body,
|
|
52
|
+
});
|
|
53
|
+
if (response.status === 429) {
|
|
54
|
+
if (attempt < MAX_RETRIES) {
|
|
55
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
56
|
+
const delayMs = retryAfter
|
|
57
|
+
? parseInt(retryAfter, 10) * 1000
|
|
58
|
+
: BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
59
|
+
await sleep(delayMs);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
throw new ApiError('UniProt API rate limit exceeded');
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new ApiError(`UniProt API returned HTTP ${response.status}: ${response.statusText}`, `Request URL: ${finalUrl}`);
|
|
66
|
+
}
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof ApiError)
|
|
71
|
+
throw err;
|
|
72
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
73
|
+
if (attempt < MAX_RETRIES) {
|
|
74
|
+
await sleep(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new ApiError(`UniProt request failed after ${MAX_RETRIES + 1} attempts: ${lastError?.message ?? 'unknown error'}`);
|
|
80
|
+
}
|
|
81
|
+
/** Create a UniProt HttpContext. */
|
|
82
|
+
function createContext() {
|
|
83
|
+
return {
|
|
84
|
+
databaseId: 'uniprot',
|
|
85
|
+
async fetch(url, opts) {
|
|
86
|
+
return uniprotFetch(url, opts);
|
|
87
|
+
},
|
|
88
|
+
async fetchJson(url, opts) {
|
|
89
|
+
const response = await uniprotFetch(url, opts);
|
|
90
|
+
return response.json();
|
|
91
|
+
},
|
|
92
|
+
async fetchXml(url, opts) {
|
|
93
|
+
const response = await uniprotFetch(url, opts);
|
|
94
|
+
return response.text(); // caller can parse XML if needed
|
|
95
|
+
},
|
|
96
|
+
async fetchText(url, opts) {
|
|
97
|
+
const response = await uniprotFetch(url, opts);
|
|
98
|
+
return response.text();
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ── Backend registration ─────────────────────────────────────────────────────
|
|
103
|
+
export const uniprotBackend = {
|
|
104
|
+
id: 'uniprot',
|
|
105
|
+
name: 'UniProt',
|
|
106
|
+
baseUrl: BASE_URL,
|
|
107
|
+
rateLimit: 50,
|
|
108
|
+
createContext,
|
|
109
|
+
};
|
|
110
|
+
registerBackend(uniprotBackend);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI discovery: finds YAML/TS CLI definitions and registers them.
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* 1. FAST PATH (manifest): If a pre-compiled cli-manifest.json exists,
|
|
6
|
+
* registers all YAML commands instantly without runtime YAML parsing.
|
|
7
|
+
* TS modules are loaded lazily only when their command is executed.
|
|
8
|
+
* 2. FALLBACK (filesystem scan): Traditional runtime discovery for development.
|
|
9
|
+
*/
|
|
10
|
+
/** Built-in CLIs directory: dist/clis/ or src/clis/ */
|
|
11
|
+
export declare const BUILTIN_CLIS_DIR: string;
|
|
12
|
+
/** User runtime directory: ~/.biocli */
|
|
13
|
+
export declare const USER_BIOCLI_DIR: string;
|
|
14
|
+
/** @deprecated Use USER_BIOCLI_DIR. */
|
|
15
|
+
export declare const USER_NCBICLI_DIR: string;
|
|
16
|
+
/** User CLIs directory: ~/.biocli/clis/ */
|
|
17
|
+
export declare const USER_CLIS_DIR: string;
|
|
18
|
+
/** Plugins directory: ~/.biocli/plugins/ */
|
|
19
|
+
export declare const PLUGINS_DIR: string;
|
|
20
|
+
/**
|
|
21
|
+
* Discover and register CLI commands.
|
|
22
|
+
* Uses pre-compiled manifest when available for instant startup.
|
|
23
|
+
*/
|
|
24
|
+
export declare function discoverClis(...dirs: string[]): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Discover and load plugins from ~/.biocli/plugins/.
|
|
27
|
+
*
|
|
28
|
+
* Each plugin is either:
|
|
29
|
+
* - A directory with a package.json (npm package)
|
|
30
|
+
* - A single .ts/.js file
|
|
31
|
+
*/
|
|
32
|
+
export declare function discoverPlugins(): Promise<void>;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI discovery: finds YAML/TS CLI definitions and registers them.
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* 1. FAST PATH (manifest): If a pre-compiled cli-manifest.json exists,
|
|
6
|
+
* registers all YAML commands instantly without runtime YAML parsing.
|
|
7
|
+
* TS modules are loaded lazily only when their command is executed.
|
|
8
|
+
* 2. FALLBACK (filesystem scan): Traditional runtime discovery for development.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
import { Strategy, registerCommand } from './registry.js';
|
|
16
|
+
import { getErrorMessage } from './errors.js';
|
|
17
|
+
import { parseYamlCli } from './yaml-schema.js';
|
|
18
|
+
import { isRecord } from './utils.js';
|
|
19
|
+
// ── Directory paths ─────────────────────────────────────────────────────────
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
/** Built-in CLIs directory: dist/clis/ or src/clis/ */
|
|
22
|
+
export const BUILTIN_CLIS_DIR = path.join(__dirname, 'clis');
|
|
23
|
+
/** User runtime directory: ~/.biocli */
|
|
24
|
+
export const USER_BIOCLI_DIR = path.join(os.homedir(), '.biocli');
|
|
25
|
+
/** @deprecated Use USER_BIOCLI_DIR. */
|
|
26
|
+
export const USER_NCBICLI_DIR = USER_BIOCLI_DIR;
|
|
27
|
+
/** User CLIs directory: ~/.biocli/clis/ */
|
|
28
|
+
export const USER_CLIS_DIR = path.join(USER_BIOCLI_DIR, 'clis');
|
|
29
|
+
/** Plugins directory: ~/.biocli/plugins/ */
|
|
30
|
+
export const PLUGINS_DIR = path.join(USER_BIOCLI_DIR, 'plugins');
|
|
31
|
+
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
32
|
+
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
33
|
+
// ── Strategy parser ─────────────────────────────────────────────────────────
|
|
34
|
+
function parseStrategy(rawStrategy) {
|
|
35
|
+
if (!rawStrategy)
|
|
36
|
+
return Strategy.PUBLIC;
|
|
37
|
+
const upper = rawStrategy.toUpperCase();
|
|
38
|
+
if (upper === 'API_KEY')
|
|
39
|
+
return Strategy.API_KEY;
|
|
40
|
+
return Strategy.PUBLIC;
|
|
41
|
+
}
|
|
42
|
+
// ── Main discovery entry point ──────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Discover and register CLI commands.
|
|
45
|
+
* Uses pre-compiled manifest when available for instant startup.
|
|
46
|
+
*/
|
|
47
|
+
export async function discoverClis(...dirs) {
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
// Fast path: try manifest first (production / post-build)
|
|
50
|
+
const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
|
|
51
|
+
try {
|
|
52
|
+
await fs.promises.access(manifestPath);
|
|
53
|
+
const loaded = await loadFromManifest(manifestPath, dir);
|
|
54
|
+
if (loaded)
|
|
55
|
+
continue; // Skip filesystem scan only when manifest is usable
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Fall through to filesystem scan
|
|
59
|
+
}
|
|
60
|
+
await discoverClisFromFs(dir);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ── Fast path: manifest loading ─────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Fast-path: register commands from pre-compiled manifest.
|
|
66
|
+
* YAML pipelines are inlined — zero YAML parsing at runtime.
|
|
67
|
+
* TS modules are deferred — loaded lazily on first execution.
|
|
68
|
+
*/
|
|
69
|
+
async function loadFromManifest(manifestPath, clisDir) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await fs.promises.readFile(manifestPath, 'utf-8');
|
|
72
|
+
const manifest = JSON.parse(raw);
|
|
73
|
+
for (const entry of manifest) {
|
|
74
|
+
if (entry.type === 'yaml') {
|
|
75
|
+
// YAML pipelines fully inlined in manifest — register directly
|
|
76
|
+
const strategy = parseStrategy(entry.strategy);
|
|
77
|
+
const cmd = {
|
|
78
|
+
site: entry.site,
|
|
79
|
+
name: entry.name,
|
|
80
|
+
aliases: entry.aliases,
|
|
81
|
+
description: entry.description ?? '',
|
|
82
|
+
database: entry.database,
|
|
83
|
+
strategy,
|
|
84
|
+
args: entry.args ?? [],
|
|
85
|
+
columns: entry.columns,
|
|
86
|
+
pipeline: entry.pipeline,
|
|
87
|
+
timeoutSeconds: entry.timeout,
|
|
88
|
+
source: `manifest:${entry.site}/${entry.name}`,
|
|
89
|
+
deprecated: entry.deprecated,
|
|
90
|
+
replacedBy: entry.replacedBy,
|
|
91
|
+
};
|
|
92
|
+
registerCommand(cmd);
|
|
93
|
+
}
|
|
94
|
+
else if (entry.type === 'ts' && entry.modulePath) {
|
|
95
|
+
// TS adapters: register a lightweight stub.
|
|
96
|
+
// The actual module is loaded lazily on first executeCommand().
|
|
97
|
+
const strategy = parseStrategy(entry.strategy ?? 'public');
|
|
98
|
+
const modulePath = path.resolve(clisDir, entry.modulePath);
|
|
99
|
+
const cmd = {
|
|
100
|
+
site: entry.site,
|
|
101
|
+
name: entry.name,
|
|
102
|
+
aliases: entry.aliases,
|
|
103
|
+
description: entry.description ?? '',
|
|
104
|
+
database: entry.database,
|
|
105
|
+
strategy,
|
|
106
|
+
args: entry.args ?? [],
|
|
107
|
+
columns: entry.columns,
|
|
108
|
+
timeoutSeconds: entry.timeout,
|
|
109
|
+
source: modulePath,
|
|
110
|
+
deprecated: entry.deprecated,
|
|
111
|
+
replacedBy: entry.replacedBy,
|
|
112
|
+
_lazy: true,
|
|
113
|
+
_modulePath: modulePath,
|
|
114
|
+
};
|
|
115
|
+
registerCommand(cmd);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(`[biocli] Failed to load manifest ${manifestPath}: ${getErrorMessage(err)}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ── Fallback: filesystem scan ───────────────────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Check if a .ts/.js file looks like a CLI module (contains cli() call).
|
|
128
|
+
*/
|
|
129
|
+
async function isCliModule(filePath) {
|
|
130
|
+
try {
|
|
131
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
132
|
+
return PLUGIN_MODULE_PATTERN.test(content);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Fallback: traditional filesystem scan (used during development with tsx).
|
|
140
|
+
*/
|
|
141
|
+
async function discoverClisFromFs(dir) {
|
|
142
|
+
try {
|
|
143
|
+
await fs.promises.access(dir);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
149
|
+
const sitePromises = entries
|
|
150
|
+
.filter(entry => entry.isDirectory())
|
|
151
|
+
.map(async (entry) => {
|
|
152
|
+
const site = entry.name;
|
|
153
|
+
// Skip hidden/shared directories
|
|
154
|
+
if (site.startsWith('.') || site.startsWith('_'))
|
|
155
|
+
return;
|
|
156
|
+
const siteDir = path.join(dir, site);
|
|
157
|
+
const files = await fs.promises.readdir(siteDir);
|
|
158
|
+
await Promise.all(files.map(async (file) => {
|
|
159
|
+
if (file.startsWith('.'))
|
|
160
|
+
return; // skip hidden/AppleDouble files
|
|
161
|
+
const filePath = path.join(siteDir, file);
|
|
162
|
+
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
163
|
+
await registerYamlCli(filePath, site);
|
|
164
|
+
}
|
|
165
|
+
else if ((file.endsWith('.js') && !file.endsWith('.d.js')) ||
|
|
166
|
+
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))) {
|
|
167
|
+
if (!(await isCliModule(filePath)))
|
|
168
|
+
return;
|
|
169
|
+
await import(pathToFileURL(filePath).href).catch((err) => {
|
|
170
|
+
console.error(`[biocli] Failed to load module ${filePath}: ${getErrorMessage(err)}`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}));
|
|
174
|
+
});
|
|
175
|
+
await Promise.all(sitePromises);
|
|
176
|
+
}
|
|
177
|
+
async function registerYamlCli(filePath, defaultSite) {
|
|
178
|
+
try {
|
|
179
|
+
const raw = await fs.promises.readFile(filePath, 'utf-8');
|
|
180
|
+
const def = yaml.load(raw);
|
|
181
|
+
if (!isRecord(def))
|
|
182
|
+
return;
|
|
183
|
+
parseYamlCli(def, filePath, defaultSite);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
console.error(`[biocli] Failed to parse YAML ${filePath}: ${getErrorMessage(err)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ── Plugin discovery ────────────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* Discover and load plugins from ~/.biocli/plugins/.
|
|
192
|
+
*
|
|
193
|
+
* Each plugin is either:
|
|
194
|
+
* - A directory with a package.json (npm package)
|
|
195
|
+
* - A single .ts/.js file
|
|
196
|
+
*/
|
|
197
|
+
export async function discoverPlugins() {
|
|
198
|
+
try {
|
|
199
|
+
await fs.promises.access(PLUGINS_DIR);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return; // No plugins directory — nothing to do
|
|
203
|
+
}
|
|
204
|
+
const entries = await fs.promises.readdir(PLUGINS_DIR, { withFileTypes: true });
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const fullPath = path.join(PLUGINS_DIR, entry.name);
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
// Directory plugin: look for package.json with "main" entry
|
|
209
|
+
const pkgPath = path.join(fullPath, 'package.json');
|
|
210
|
+
try {
|
|
211
|
+
await fs.promises.access(pkgPath);
|
|
212
|
+
const pkg = JSON.parse(await fs.promises.readFile(pkgPath, 'utf-8'));
|
|
213
|
+
const mainFile = pkg.main ?? 'index.js';
|
|
214
|
+
const mainPath = path.resolve(fullPath, mainFile);
|
|
215
|
+
await import(pathToFileURL(mainPath).href);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
console.error(`[biocli] Failed to load plugin ${entry.name}: ${getErrorMessage(err)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if ((entry.name.endsWith('.js') && !entry.name.endsWith('.d.js')) ||
|
|
222
|
+
(entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts'))) {
|
|
223
|
+
// Single-file plugin
|
|
224
|
+
try {
|
|
225
|
+
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
|
226
|
+
if (PLUGIN_MODULE_PATTERN.test(content)) {
|
|
227
|
+
await import(pathToFileURL(fullPath).href);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
console.error(`[biocli] Failed to load plugin ${entry.name}: ${getErrorMessage(err)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* biocli doctor — Diagnose configuration and backend connectivity.
|
|
3
|
+
*
|
|
4
|
+
* Checks Node.js version, config status, API key/email, and
|
|
5
|
+
* reachability of all 6 database backends in parallel.
|
|
6
|
+
*/
|
|
7
|
+
interface CheckResult {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
ok: boolean;
|
|
11
|
+
detail?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function runDoctor(): Promise<{
|
|
14
|
+
checks: CheckResult[];
|
|
15
|
+
allPassed: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function formatDoctorText(checks: CheckResult[], allPassed: boolean): string;
|
|
18
|
+
export declare function formatDoctorJson(checks: CheckResult[], allPassed: boolean): string;
|
|
19
|
+
export {};
|