@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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander adapter: bridges Registry commands to Commander subcommands.
|
|
3
|
+
*
|
|
4
|
+
* This is a THIN adapter — it only handles:
|
|
5
|
+
* 1. Commander arg/option registration
|
|
6
|
+
* 2. Collecting kwargs from Commander's action args
|
|
7
|
+
* 3. Calling executeCommand (which handles HttpContext, validation, etc.)
|
|
8
|
+
* 4. Rendering output and errors
|
|
9
|
+
*
|
|
10
|
+
* All execution logic lives in execution.ts.
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { parseBatchInput, mergeBatchResults } from './batch.js';
|
|
14
|
+
import { fullName, getRegistry } from './registry.js';
|
|
15
|
+
import { render as renderOutput } from './output.js';
|
|
16
|
+
import { executeCommand } from './execution.js';
|
|
17
|
+
import { startSpinner } from './spinner.js';
|
|
18
|
+
import { hasResultMeta } from './types.js';
|
|
19
|
+
import { CliError, EXIT_CODES, ERROR_ICONS, getErrorMessage, ArgumentError, } from './errors.js';
|
|
20
|
+
// ── Arg value normalization ─────────────────────────────────────────────────
|
|
21
|
+
export function normalizeArgValue(argType, value, name) {
|
|
22
|
+
if (argType !== 'bool' && argType !== 'boolean')
|
|
23
|
+
return value;
|
|
24
|
+
if (typeof value === 'boolean')
|
|
25
|
+
return value;
|
|
26
|
+
if (value == null || value === '')
|
|
27
|
+
return false;
|
|
28
|
+
const normalized = String(value).trim().toLowerCase();
|
|
29
|
+
if (normalized === 'true')
|
|
30
|
+
return true;
|
|
31
|
+
if (normalized === 'false')
|
|
32
|
+
return false;
|
|
33
|
+
throw new ArgumentError(`"${name}" must be either "true" or "false".`);
|
|
34
|
+
}
|
|
35
|
+
// ── Register a single command ───────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Register a single CliCommand as a Commander subcommand.
|
|
38
|
+
*/
|
|
39
|
+
export function registerCommandToProgram(siteCmd, cmd) {
|
|
40
|
+
if (siteCmd.commands.some((c) => c.name() === cmd.name))
|
|
41
|
+
return;
|
|
42
|
+
const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
|
|
43
|
+
const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
|
|
44
|
+
if (cmd.aliases?.length)
|
|
45
|
+
subCmd.aliases(cmd.aliases);
|
|
46
|
+
// Register positional args first, then named options
|
|
47
|
+
// Positional args are always registered as optional with Commander —
|
|
48
|
+
// required checks are done in the action handler to allow --input batch mode
|
|
49
|
+
const positionalArgs = [];
|
|
50
|
+
for (const arg of cmd.args) {
|
|
51
|
+
if (arg.positional) {
|
|
52
|
+
subCmd.argument(`[${arg.name}]`, arg.help ?? '');
|
|
53
|
+
positionalArgs.push(arg);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
57
|
+
if (arg.required)
|
|
58
|
+
subCmd.requiredOption(flag, arg.help ?? '');
|
|
59
|
+
else if (arg.default != null)
|
|
60
|
+
subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
61
|
+
else
|
|
62
|
+
subCmd.option(flag, arg.help ?? '');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
subCmd
|
|
66
|
+
.option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
|
|
67
|
+
.option('-c, --columns <cols>', 'Columns to display (comma-separated, e.g. pmid,title,abstract)')
|
|
68
|
+
.option('-A, --all-columns', 'Show all available columns', false)
|
|
69
|
+
.option('-v, --verbose', 'Debug output', false)
|
|
70
|
+
.option('--input <file>', 'Batch input: file with one ID per line, or - for stdin')
|
|
71
|
+
.option('--no-cache', 'Skip cache and fetch fresh data');
|
|
72
|
+
subCmd.action(async (...actionArgs) => {
|
|
73
|
+
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
74
|
+
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts : {};
|
|
75
|
+
const startTime = Date.now();
|
|
76
|
+
// ── Execute + render ────────────────────────────────────────────────
|
|
77
|
+
try {
|
|
78
|
+
// ── Collect kwargs ────────────────────────────────────────────────
|
|
79
|
+
const kwargs = {};
|
|
80
|
+
for (let i = 0; i < positionalArgs.length; i++) {
|
|
81
|
+
const v = actionArgs[i];
|
|
82
|
+
if (v !== undefined)
|
|
83
|
+
kwargs[positionalArgs[i].name] = v;
|
|
84
|
+
}
|
|
85
|
+
for (const arg of cmd.args) {
|
|
86
|
+
if (arg.positional)
|
|
87
|
+
continue;
|
|
88
|
+
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
|
|
89
|
+
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
|
|
90
|
+
if (v !== undefined)
|
|
91
|
+
kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
|
|
92
|
+
}
|
|
93
|
+
const verbose = optionsRecord.verbose === true;
|
|
94
|
+
const inputFile = typeof optionsRecord.input === 'string' ? optionsRecord.input : undefined;
|
|
95
|
+
// Validate required positional args (unless --input provides batch input)
|
|
96
|
+
if (!inputFile) {
|
|
97
|
+
for (const arg of positionalArgs) {
|
|
98
|
+
if (arg.required && (kwargs[arg.name] === undefined || kwargs[arg.name] === null || kwargs[arg.name] === '')) {
|
|
99
|
+
console.error(chalk.red(`error: missing required argument '${arg.name}'`));
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
|
|
106
|
+
if (verbose)
|
|
107
|
+
process.env.BIOCLI_VERBOSE = '1';
|
|
108
|
+
if (cmd.deprecated) {
|
|
109
|
+
const message = typeof cmd.deprecated === 'string' ? cmd.deprecated : `${fullName(cmd)} is deprecated.`;
|
|
110
|
+
const replacement = cmd.replacedBy ? ` Use ${cmd.replacedBy} instead.` : '';
|
|
111
|
+
console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
|
|
112
|
+
}
|
|
113
|
+
// Commander's --no-cache sets optionsRecord.cache to false
|
|
114
|
+
const noCache = optionsRecord.cache === false;
|
|
115
|
+
// ── Batch mode: --input or comma-separated positional ────────────
|
|
116
|
+
const primaryArg = positionalArgs[0]; // first positional = primary ID/query
|
|
117
|
+
const batchItems = primaryArg
|
|
118
|
+
? parseBatchInput(kwargs[primaryArg.name], inputFile)
|
|
119
|
+
: null;
|
|
120
|
+
let result;
|
|
121
|
+
if (batchItems && primaryArg) {
|
|
122
|
+
const spinnerLabel = `Batch ${fullName(cmd)} (${batchItems.length} items)…`;
|
|
123
|
+
const spinner = startSpinner(spinnerLabel);
|
|
124
|
+
const batchResults = [];
|
|
125
|
+
const errors = [];
|
|
126
|
+
try {
|
|
127
|
+
for (const item of batchItems) {
|
|
128
|
+
try {
|
|
129
|
+
const batchKwargs = { ...kwargs, [primaryArg.name]: item };
|
|
130
|
+
const r = await executeCommand(cmd, batchKwargs, verbose, { noCache });
|
|
131
|
+
if (r !== null && r !== undefined)
|
|
132
|
+
batchResults.push(r);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
errors.push(`${item}: ${err instanceof Error ? err.message : String(err)}`);
|
|
136
|
+
if (verbose)
|
|
137
|
+
console.error(chalk.yellow(`[Batch] ${item} failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
spinner.stop();
|
|
143
|
+
}
|
|
144
|
+
if (errors.length > 0) {
|
|
145
|
+
console.error(chalk.yellow(`[Batch] ${errors.length}/${batchItems.length} failed`));
|
|
146
|
+
if (verbose)
|
|
147
|
+
errors.forEach(e => console.error(chalk.dim(` ${e}`)));
|
|
148
|
+
}
|
|
149
|
+
if (!batchResults.length) {
|
|
150
|
+
console.error(chalk.red(`All ${batchItems.length} batch items failed.`));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
result = mergeBatchResults(batchResults);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const spinnerLabel = cmd.database
|
|
158
|
+
? `Querying ${cmd.database}…`
|
|
159
|
+
: `Running ${fullName(cmd)}…`;
|
|
160
|
+
const spinner = startSpinner(spinnerLabel);
|
|
161
|
+
try {
|
|
162
|
+
result = await executeCommand(cmd, kwargs, verbose, { noCache });
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
spinner.stop();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (result === null || result === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Extract display metadata if the command returned ResultWithMeta
|
|
172
|
+
let renderData = result;
|
|
173
|
+
let totalCount;
|
|
174
|
+
let query;
|
|
175
|
+
if (hasResultMeta(result)) {
|
|
176
|
+
renderData = result.rows;
|
|
177
|
+
totalCount = result.meta.totalCount;
|
|
178
|
+
query = result.meta.query;
|
|
179
|
+
}
|
|
180
|
+
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
181
|
+
if (format === 'table' && resolved.defaultFormat) {
|
|
182
|
+
format = resolved.defaultFormat;
|
|
183
|
+
}
|
|
184
|
+
// Auto-detect pipe: output JSON when stdout is not a terminal
|
|
185
|
+
if (format === 'table' && !process.stdout.isTTY) {
|
|
186
|
+
format = 'json';
|
|
187
|
+
}
|
|
188
|
+
if (verbose && (!renderData || (Array.isArray(renderData) && renderData.length === 0))) {
|
|
189
|
+
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
|
|
190
|
+
}
|
|
191
|
+
// Resolve which columns to display:
|
|
192
|
+
// --columns pmid,title,abstract → user-specified subset
|
|
193
|
+
// --all-columns / -A → all keys from first row
|
|
194
|
+
// (default) → adapter-declared columns
|
|
195
|
+
let displayColumns = resolved.columns;
|
|
196
|
+
const allColumns = optionsRecord.allColumns === true;
|
|
197
|
+
const userColumns = typeof optionsRecord.columns === 'string' ? optionsRecord.columns : undefined;
|
|
198
|
+
if (userColumns) {
|
|
199
|
+
displayColumns = userColumns.split(',').map((s) => s.trim()).filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
else if (allColumns) {
|
|
202
|
+
// Show all fields present in the data
|
|
203
|
+
displayColumns = undefined; // output.ts will derive from row keys
|
|
204
|
+
}
|
|
205
|
+
renderOutput(renderData, {
|
|
206
|
+
fmt: format,
|
|
207
|
+
columns: displayColumns,
|
|
208
|
+
title: `${resolved.site}/${resolved.name}`,
|
|
209
|
+
elapsed: (Date.now() - startTime) / 1000,
|
|
210
|
+
source: fullName(resolved),
|
|
211
|
+
totalCount,
|
|
212
|
+
query,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
renderError(err, fullName(cmd), optionsRecord.verbose === true);
|
|
217
|
+
process.exitCode = resolveExitCode(err);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// ── Register all commands ───────────────────────────────────────────────────
|
|
222
|
+
/**
|
|
223
|
+
* Iterate the registry, group commands by site, and register each
|
|
224
|
+
* as a Commander subcommand under a site parent command.
|
|
225
|
+
*/
|
|
226
|
+
export function registerAllCommands(program) {
|
|
227
|
+
const registry = getRegistry();
|
|
228
|
+
const sites = new Map();
|
|
229
|
+
// Deduplicate: skip alias entries (they point to the same CliCommand object)
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
for (const [, cmd] of registry) {
|
|
232
|
+
if (seen.has(cmd))
|
|
233
|
+
continue;
|
|
234
|
+
seen.add(cmd);
|
|
235
|
+
const group = sites.get(cmd.site) ?? [];
|
|
236
|
+
group.push(cmd);
|
|
237
|
+
sites.set(cmd.site, group);
|
|
238
|
+
}
|
|
239
|
+
for (const [site, commands] of sites) {
|
|
240
|
+
// Create parent command for the site (e.g. "pubmed", "gene")
|
|
241
|
+
let siteCmd = program.commands.find(c => c.name() === site);
|
|
242
|
+
if (!siteCmd) {
|
|
243
|
+
siteCmd = program.command(site).description(`${site} commands`);
|
|
244
|
+
}
|
|
245
|
+
for (const cmd of commands) {
|
|
246
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ── Exit code resolution ─────────────────────────────────────────────────────
|
|
251
|
+
/**
|
|
252
|
+
* Map any thrown value to a Unix process exit code.
|
|
253
|
+
*/
|
|
254
|
+
export function resolveExitCode(err) {
|
|
255
|
+
if (err instanceof CliError)
|
|
256
|
+
return err.exitCode;
|
|
257
|
+
const msg = getErrorMessage(err);
|
|
258
|
+
const m = msg.toLowerCase();
|
|
259
|
+
if (/\b(status[: ]+)?[45]\d{2}\b|http[/ ][45]\d{2}/.test(m))
|
|
260
|
+
return EXIT_CODES.GENERIC_ERROR;
|
|
261
|
+
if (/not found|no .+ found/.test(m))
|
|
262
|
+
return EXIT_CODES.EMPTY_RESULT;
|
|
263
|
+
return EXIT_CODES.GENERIC_ERROR;
|
|
264
|
+
}
|
|
265
|
+
// ── Error rendering ──────────────────────────────────────────────────────────
|
|
266
|
+
const ISSUES_URL = 'https://github.com/youngfly93/biocli/issues';
|
|
267
|
+
function renderError(err, cmdName, verbose) {
|
|
268
|
+
if (err instanceof CliError) {
|
|
269
|
+
const icon = ERROR_ICONS[err.code] ?? '!';
|
|
270
|
+
console.error(chalk.red(`${icon} ${err.message}`));
|
|
271
|
+
if (err.hint) {
|
|
272
|
+
console.error(chalk.yellow(` Hint: ${err.hint}`));
|
|
273
|
+
}
|
|
274
|
+
if (verbose && err.stack) {
|
|
275
|
+
console.error(chalk.dim(err.stack));
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Generic error
|
|
280
|
+
const message = getErrorMessage(err);
|
|
281
|
+
console.error(chalk.red(`! Error in ${cmdName}: ${message}`));
|
|
282
|
+
if (verbose && err instanceof Error && err.stack) {
|
|
283
|
+
console.error(chalk.dim(err.stack));
|
|
284
|
+
}
|
|
285
|
+
console.error(chalk.dim(` If this persists, please report at ${ISSUES_URL}`));
|
|
286
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell tab-completion support for biocli.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Shell script generators for bash, zsh, and fish
|
|
6
|
+
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Return completion candidates given the current command-line words and cursor index.
|
|
10
|
+
*
|
|
11
|
+
* @param words - The argv after 'biocli' (words[0] is the first arg, e.g. site name)
|
|
12
|
+
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
13
|
+
*/
|
|
14
|
+
export declare function getCompletions(words: string[], cursor: number): string[];
|
|
15
|
+
export declare function generateCompletion(shell: 'bash' | 'zsh' | 'fish'): string;
|
|
16
|
+
/**
|
|
17
|
+
* Print the completion script for the requested shell.
|
|
18
|
+
*/
|
|
19
|
+
export declare function printCompletionScript(shell: string): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell tab-completion support for biocli.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Shell script generators for bash, zsh, and fish
|
|
6
|
+
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
|
+
*/
|
|
8
|
+
import { getRegistry } from './registry.js';
|
|
9
|
+
// ── Dynamic completion logic ───────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Built-in (non-dynamic) top-level commands.
|
|
12
|
+
*/
|
|
13
|
+
const BUILTIN_COMMANDS = [
|
|
14
|
+
'list',
|
|
15
|
+
'validate',
|
|
16
|
+
'config',
|
|
17
|
+
'completion',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Return completion candidates given the current command-line words and cursor index.
|
|
21
|
+
*
|
|
22
|
+
* @param words - The argv after 'biocli' (words[0] is the first arg, e.g. site name)
|
|
23
|
+
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
24
|
+
*/
|
|
25
|
+
export function getCompletions(words, cursor) {
|
|
26
|
+
// cursor === 1 → completing the first argument (site name or built-in command)
|
|
27
|
+
if (cursor <= 1) {
|
|
28
|
+
const sites = new Set();
|
|
29
|
+
for (const [, cmd] of getRegistry()) {
|
|
30
|
+
sites.add(cmd.site);
|
|
31
|
+
}
|
|
32
|
+
return [...BUILTIN_COMMANDS, ...sites].sort();
|
|
33
|
+
}
|
|
34
|
+
const site = words[0];
|
|
35
|
+
// If the first word is a built-in command, no further completion
|
|
36
|
+
if (BUILTIN_COMMANDS.includes(site)) {
|
|
37
|
+
// Special case: 'config' has subcommands
|
|
38
|
+
if (site === 'config' && cursor === 2) {
|
|
39
|
+
return ['show', 'set', 'path'];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
// cursor === 2 → completing the sub-command name under a site
|
|
44
|
+
if (cursor === 2) {
|
|
45
|
+
const subcommands = [];
|
|
46
|
+
for (const [, cmd] of getRegistry()) {
|
|
47
|
+
if (cmd.site === site) {
|
|
48
|
+
subcommands.push(cmd.name);
|
|
49
|
+
if (cmd.aliases?.length)
|
|
50
|
+
subcommands.push(...cmd.aliases);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...new Set(subcommands)].sort();
|
|
54
|
+
}
|
|
55
|
+
// cursor >= 3 → no further completion
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
// ── Shell script generators ────────────────────────────────────────────────
|
|
59
|
+
export function generateCompletion(shell) {
|
|
60
|
+
switch (shell) {
|
|
61
|
+
case 'bash':
|
|
62
|
+
return bashCompletionScript();
|
|
63
|
+
case 'zsh':
|
|
64
|
+
return zshCompletionScript();
|
|
65
|
+
case 'fish':
|
|
66
|
+
return fishCompletionScript();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function bashCompletionScript() {
|
|
70
|
+
return `# Bash completion for biocli
|
|
71
|
+
# Add to ~/.bashrc: eval "$(biocli completion bash)"
|
|
72
|
+
_biocli_completions() {
|
|
73
|
+
local cur words cword
|
|
74
|
+
_get_comp_words_by_ref -n : cur words cword
|
|
75
|
+
|
|
76
|
+
local completions
|
|
77
|
+
completions=$(biocli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
|
|
78
|
+
|
|
79
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
|
|
80
|
+
__ltrim_colon_completions "$cur"
|
|
81
|
+
}
|
|
82
|
+
complete -F _biocli_completions biocli
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
function zshCompletionScript() {
|
|
86
|
+
return `# Zsh completion for biocli
|
|
87
|
+
# Add to ~/.zshrc: eval "$(biocli completion zsh)"
|
|
88
|
+
_biocli() {
|
|
89
|
+
local -a completions
|
|
90
|
+
local cword=$((CURRENT - 1))
|
|
91
|
+
completions=(\${(f)"$(biocli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
|
|
92
|
+
compadd -a completions
|
|
93
|
+
}
|
|
94
|
+
compdef _biocli biocli
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
function fishCompletionScript() {
|
|
98
|
+
return `# Fish completion for biocli
|
|
99
|
+
# Add to ~/.config/fish/config.fish: biocli completion fish | source
|
|
100
|
+
complete -c biocli -f -a '(
|
|
101
|
+
set -l tokens (commandline -cop)
|
|
102
|
+
set -l cursor (count (commandline -cop))
|
|
103
|
+
biocli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
|
|
104
|
+
)'
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Print the completion script for the requested shell.
|
|
109
|
+
*/
|
|
110
|
+
export function printCompletionScript(shell) {
|
|
111
|
+
if (shell !== 'bash' && shell !== 'zsh' && shell !== 'fish') {
|
|
112
|
+
console.error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write(generateCompletion(shell));
|
|
117
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for biocli.
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes ~/.biocli/config.yaml for persistent settings
|
|
5
|
+
* such as API keys, email, and default output preferences.
|
|
6
|
+
*
|
|
7
|
+
* Priority for NCBI credentials:
|
|
8
|
+
* 1. Environment variables (NCBI_API_KEY, NCBI_EMAIL)
|
|
9
|
+
* 2. Config file (~/.biocli/config.yaml)
|
|
10
|
+
*
|
|
11
|
+
* Migration: if ~/.ncbicli/config.yaml exists and ~/.biocli/ does not,
|
|
12
|
+
* the old config is automatically migrated on first load.
|
|
13
|
+
*/
|
|
14
|
+
export interface BiocliConfig {
|
|
15
|
+
/** NCBI API key — get one at https://www.ncbi.nlm.nih.gov/account/settings/ */
|
|
16
|
+
api_key?: string;
|
|
17
|
+
/** Contact email (recommended by NCBI for E-utilities usage). */
|
|
18
|
+
email?: string;
|
|
19
|
+
/** Default settings applied when the user does not provide explicit flags. */
|
|
20
|
+
defaults?: {
|
|
21
|
+
/** Default output format (json, table, csv, etc.). */
|
|
22
|
+
format?: string;
|
|
23
|
+
/** Default maximum number of results to return. */
|
|
24
|
+
limit?: number;
|
|
25
|
+
};
|
|
26
|
+
/** Cache settings. */
|
|
27
|
+
cache?: {
|
|
28
|
+
/** Whether caching is enabled (default: true). */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/** Cache TTL in hours (default: 24). */
|
|
31
|
+
ttl?: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** @deprecated Use BiocliConfig instead. */
|
|
35
|
+
export type NcbiConfig = BiocliConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Load the config file from disk. Returns an empty object if the file
|
|
38
|
+
* does not exist or cannot be parsed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadConfig(): BiocliConfig;
|
|
41
|
+
/**
|
|
42
|
+
* Persist the given config object to ~/.biocli/config.yaml.
|
|
43
|
+
* Creates the directory if it does not exist.
|
|
44
|
+
*/
|
|
45
|
+
export declare function saveConfig(config: BiocliConfig): void;
|
|
46
|
+
/** Return the absolute path to the config file. */
|
|
47
|
+
export declare function getConfigPath(): string;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the NCBI API key.
|
|
50
|
+
* Priority: env NCBI_API_KEY > config file api_key field.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getApiKey(): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the contact email.
|
|
55
|
+
* Priority: env NCBI_EMAIL > config file email field.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getEmail(): string | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for biocli.
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes ~/.biocli/config.yaml for persistent settings
|
|
5
|
+
* such as API keys, email, and default output preferences.
|
|
6
|
+
*
|
|
7
|
+
* Priority for NCBI credentials:
|
|
8
|
+
* 1. Environment variables (NCBI_API_KEY, NCBI_EMAIL)
|
|
9
|
+
* 2. Config file (~/.biocli/config.yaml)
|
|
10
|
+
*
|
|
11
|
+
* Migration: if ~/.ncbicli/config.yaml exists and ~/.biocli/ does not,
|
|
12
|
+
* the old config is automatically migrated on first load.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import yaml from 'js-yaml';
|
|
18
|
+
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
19
|
+
const CONFIG_DIR = join(homedir(), '.biocli');
|
|
20
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
|
|
21
|
+
const LEGACY_CONFIG_DIR = join(homedir(), '.ncbicli');
|
|
22
|
+
const LEGACY_CONFIG_FILE = join(LEGACY_CONFIG_DIR, 'config.yaml');
|
|
23
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Migrate legacy ~/.ncbicli/config.yaml → ~/.biocli/config.yaml (one-time).
|
|
26
|
+
*/
|
|
27
|
+
function migrateIfNeeded() {
|
|
28
|
+
if (existsSync(CONFIG_DIR))
|
|
29
|
+
return; // already migrated or fresh install
|
|
30
|
+
if (!existsSync(LEGACY_CONFIG_FILE))
|
|
31
|
+
return; // nothing to migrate
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
cpSync(LEGACY_CONFIG_FILE, CONFIG_FILE);
|
|
35
|
+
console.error(`Migrated config: ${LEGACY_CONFIG_FILE} → ${CONFIG_FILE}`);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Non-fatal — user can manually copy
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Load the config file from disk. Returns an empty object if the file
|
|
43
|
+
* does not exist or cannot be parsed.
|
|
44
|
+
*/
|
|
45
|
+
export function loadConfig() {
|
|
46
|
+
migrateIfNeeded();
|
|
47
|
+
if (!existsSync(CONFIG_FILE))
|
|
48
|
+
return {};
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
51
|
+
const parsed = yaml.load(raw);
|
|
52
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Persist the given config object to ~/.biocli/config.yaml.
|
|
63
|
+
* Creates the directory if it does not exist.
|
|
64
|
+
*/
|
|
65
|
+
export function saveConfig(config) {
|
|
66
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
67
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
const content = yaml.dump(config, {
|
|
70
|
+
indent: 2,
|
|
71
|
+
lineWidth: 120,
|
|
72
|
+
noRefs: true,
|
|
73
|
+
sortKeys: true,
|
|
74
|
+
});
|
|
75
|
+
writeFileSync(CONFIG_FILE, content, 'utf-8');
|
|
76
|
+
}
|
|
77
|
+
/** Return the absolute path to the config file. */
|
|
78
|
+
export function getConfigPath() {
|
|
79
|
+
return CONFIG_FILE;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the NCBI API key.
|
|
83
|
+
* Priority: env NCBI_API_KEY > config file api_key field.
|
|
84
|
+
*/
|
|
85
|
+
export function getApiKey() {
|
|
86
|
+
return process.env.NCBI_API_KEY || loadConfig().api_key || undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the contact email.
|
|
90
|
+
* Priority: env NCBI_EMAIL > config file email field.
|
|
91
|
+
*/
|
|
92
|
+
export function getEmail() {
|
|
93
|
+
return process.env.NCBI_EMAIL || loadConfig().email || undefined;
|
|
94
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrichr database backend for biocli.
|
|
3
|
+
*
|
|
4
|
+
* Enrichr API (https://maayanlab.cloud/Enrichr):
|
|
5
|
+
* - No authentication required
|
|
6
|
+
* - Rate limit: undocumented (we use 5/s conservatively)
|
|
7
|
+
* - 2-step workflow: POST /addList → GET /enrich
|
|
8
|
+
* - Response format: JSON
|
|
9
|
+
*
|
|
10
|
+
* Popular gene set libraries:
|
|
11
|
+
* KEGG_2021_Human, GO_Biological_Process_2023, GO_Molecular_Function_2023,
|
|
12
|
+
* GO_Cellular_Component_2023, WikiPathway_2023_Human, Reactome_2022,
|
|
13
|
+
* MSigDB_Hallmark_2020, DisGeNET, OMIM_Disease, GWAS_Catalog_2023
|
|
14
|
+
*/
|
|
15
|
+
import { type DatabaseBackend } from './index.js';
|
|
16
|
+
/**
|
|
17
|
+
* Submit a gene list to Enrichr and return the userListId.
|
|
18
|
+
* This is step 1 of the 2-step workflow.
|
|
19
|
+
*
|
|
20
|
+
* NOTE: Enrichr requires multipart/form-data (not URL-encoded).
|
|
21
|
+
*/
|
|
22
|
+
export declare function submitGeneList(genes: string[], description?: string): Promise<number>;
|
|
23
|
+
/**
|
|
24
|
+
* Get enrichment results for a submitted gene list.
|
|
25
|
+
* This is step 2 of the 2-step workflow.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getEnrichment(userListId: number, library: string): Promise<Record<string, unknown>[]>;
|
|
28
|
+
export declare const enrichrBackend: DatabaseBackend;
|