@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.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/batch.d.ts +20 -0
  4. package/dist/batch.js +69 -0
  5. package/dist/build-manifest.d.ts +38 -0
  6. package/dist/build-manifest.js +186 -0
  7. package/dist/cache.d.ts +28 -0
  8. package/dist/cache.js +126 -0
  9. package/dist/cli-manifest.json +1500 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.js +336 -0
  12. package/dist/clis/_shared/common.d.ts +8 -0
  13. package/dist/clis/_shared/common.js +13 -0
  14. package/dist/clis/_shared/eutils.d.ts +9 -0
  15. package/dist/clis/_shared/eutils.js +9 -0
  16. package/dist/clis/_shared/organism-db.d.ts +23 -0
  17. package/dist/clis/_shared/organism-db.js +58 -0
  18. package/dist/clis/_shared/xml-helpers.d.ts +58 -0
  19. package/dist/clis/_shared/xml-helpers.js +266 -0
  20. package/dist/clis/aggregate/enrichment.d.ts +7 -0
  21. package/dist/clis/aggregate/enrichment.js +105 -0
  22. package/dist/clis/aggregate/gene-dossier.d.ts +13 -0
  23. package/dist/clis/aggregate/gene-dossier.js +248 -0
  24. package/dist/clis/aggregate/gene-profile.d.ts +16 -0
  25. package/dist/clis/aggregate/gene-profile.js +305 -0
  26. package/dist/clis/aggregate/literature-brief.d.ts +7 -0
  27. package/dist/clis/aggregate/literature-brief.js +79 -0
  28. package/dist/clis/aggregate/variant-dossier.d.ts +11 -0
  29. package/dist/clis/aggregate/variant-dossier.js +161 -0
  30. package/dist/clis/aggregate/variant-interpret.d.ts +10 -0
  31. package/dist/clis/aggregate/variant-interpret.js +210 -0
  32. package/dist/clis/aggregate/workflow-prepare.d.ts +12 -0
  33. package/dist/clis/aggregate/workflow-prepare.js +228 -0
  34. package/dist/clis/aggregate/workflow-scout.d.ts +13 -0
  35. package/dist/clis/aggregate/workflow-scout.js +175 -0
  36. package/dist/clis/clinvar/search.d.ts +8 -0
  37. package/dist/clis/clinvar/search.js +61 -0
  38. package/dist/clis/clinvar/variant.d.ts +7 -0
  39. package/dist/clis/clinvar/variant.js +53 -0
  40. package/dist/clis/enrichr/analyze.d.ts +7 -0
  41. package/dist/clis/enrichr/analyze.js +48 -0
  42. package/dist/clis/ensembl/lookup.d.ts +6 -0
  43. package/dist/clis/ensembl/lookup.js +38 -0
  44. package/dist/clis/ensembl/vep.d.ts +7 -0
  45. package/dist/clis/ensembl/vep.js +86 -0
  46. package/dist/clis/ensembl/xrefs.d.ts +6 -0
  47. package/dist/clis/ensembl/xrefs.js +36 -0
  48. package/dist/clis/gene/fetch.d.ts +10 -0
  49. package/dist/clis/gene/fetch.js +96 -0
  50. package/dist/clis/gene/info.d.ts +7 -0
  51. package/dist/clis/gene/info.js +37 -0
  52. package/dist/clis/gene/search.d.ts +7 -0
  53. package/dist/clis/gene/search.js +71 -0
  54. package/dist/clis/geo/dataset.d.ts +7 -0
  55. package/dist/clis/geo/dataset.js +55 -0
  56. package/dist/clis/geo/download.d.ts +17 -0
  57. package/dist/clis/geo/download.js +115 -0
  58. package/dist/clis/geo/samples.d.ts +7 -0
  59. package/dist/clis/geo/samples.js +57 -0
  60. package/dist/clis/geo/search.d.ts +8 -0
  61. package/dist/clis/geo/search.js +66 -0
  62. package/dist/clis/kegg/convert.d.ts +7 -0
  63. package/dist/clis/kegg/convert.js +37 -0
  64. package/dist/clis/kegg/disease.d.ts +6 -0
  65. package/dist/clis/kegg/disease.js +57 -0
  66. package/dist/clis/kegg/link.d.ts +7 -0
  67. package/dist/clis/kegg/link.js +36 -0
  68. package/dist/clis/kegg/pathway.d.ts +6 -0
  69. package/dist/clis/kegg/pathway.js +37 -0
  70. package/dist/clis/pubmed/abstract.d.ts +7 -0
  71. package/dist/clis/pubmed/abstract.js +42 -0
  72. package/dist/clis/pubmed/cited-by.d.ts +7 -0
  73. package/dist/clis/pubmed/cited-by.js +77 -0
  74. package/dist/clis/pubmed/fetch.d.ts +6 -0
  75. package/dist/clis/pubmed/fetch.js +36 -0
  76. package/dist/clis/pubmed/info.yaml +22 -0
  77. package/dist/clis/pubmed/related.d.ts +7 -0
  78. package/dist/clis/pubmed/related.js +81 -0
  79. package/dist/clis/pubmed/search.d.ts +8 -0
  80. package/dist/clis/pubmed/search.js +63 -0
  81. package/dist/clis/snp/lookup.d.ts +7 -0
  82. package/dist/clis/snp/lookup.js +57 -0
  83. package/dist/clis/sra/download.d.ts +18 -0
  84. package/dist/clis/sra/download.js +217 -0
  85. package/dist/clis/sra/run.d.ts +8 -0
  86. package/dist/clis/sra/run.js +77 -0
  87. package/dist/clis/sra/search.d.ts +8 -0
  88. package/dist/clis/sra/search.js +83 -0
  89. package/dist/clis/string/enrichment.d.ts +7 -0
  90. package/dist/clis/string/enrichment.js +50 -0
  91. package/dist/clis/string/network.d.ts +7 -0
  92. package/dist/clis/string/network.js +47 -0
  93. package/dist/clis/string/partners.d.ts +4 -0
  94. package/dist/clis/string/partners.js +44 -0
  95. package/dist/clis/taxonomy/lookup.d.ts +8 -0
  96. package/dist/clis/taxonomy/lookup.js +54 -0
  97. package/dist/clis/uniprot/fetch.d.ts +7 -0
  98. package/dist/clis/uniprot/fetch.js +82 -0
  99. package/dist/clis/uniprot/search.d.ts +6 -0
  100. package/dist/clis/uniprot/search.js +65 -0
  101. package/dist/clis/uniprot/sequence.d.ts +7 -0
  102. package/dist/clis/uniprot/sequence.js +51 -0
  103. package/dist/commander-adapter.d.ts +27 -0
  104. package/dist/commander-adapter.js +286 -0
  105. package/dist/completion.d.ts +19 -0
  106. package/dist/completion.js +117 -0
  107. package/dist/config.d.ts +57 -0
  108. package/dist/config.js +94 -0
  109. package/dist/databases/enrichr.d.ts +28 -0
  110. package/dist/databases/enrichr.js +131 -0
  111. package/dist/databases/ensembl.d.ts +14 -0
  112. package/dist/databases/ensembl.js +106 -0
  113. package/dist/databases/index.d.ts +45 -0
  114. package/dist/databases/index.js +49 -0
  115. package/dist/databases/kegg.d.ts +26 -0
  116. package/dist/databases/kegg.js +136 -0
  117. package/dist/databases/ncbi.d.ts +28 -0
  118. package/dist/databases/ncbi.js +144 -0
  119. package/dist/databases/string-db.d.ts +19 -0
  120. package/dist/databases/string-db.js +105 -0
  121. package/dist/databases/uniprot.d.ts +13 -0
  122. package/dist/databases/uniprot.js +110 -0
  123. package/dist/discovery.d.ts +32 -0
  124. package/dist/discovery.js +235 -0
  125. package/dist/doctor.d.ts +19 -0
  126. package/dist/doctor.js +151 -0
  127. package/dist/errors.d.ts +68 -0
  128. package/dist/errors.js +105 -0
  129. package/dist/execution.d.ts +15 -0
  130. package/dist/execution.js +178 -0
  131. package/dist/hooks.d.ts +48 -0
  132. package/dist/hooks.js +58 -0
  133. package/dist/main.d.ts +13 -0
  134. package/dist/main.js +31 -0
  135. package/dist/ncbi-fetch.d.ts +10 -0
  136. package/dist/ncbi-fetch.js +10 -0
  137. package/dist/output.d.ts +18 -0
  138. package/dist/output.js +394 -0
  139. package/dist/pipeline/executor.d.ts +22 -0
  140. package/dist/pipeline/executor.js +40 -0
  141. package/dist/pipeline/index.d.ts +6 -0
  142. package/dist/pipeline/index.js +6 -0
  143. package/dist/pipeline/registry.d.ts +16 -0
  144. package/dist/pipeline/registry.js +31 -0
  145. package/dist/pipeline/steps/fetch.d.ts +21 -0
  146. package/dist/pipeline/steps/fetch.js +160 -0
  147. package/dist/pipeline/steps/transform.d.ts +26 -0
  148. package/dist/pipeline/steps/transform.js +92 -0
  149. package/dist/pipeline/steps/xml-parse.d.ts +12 -0
  150. package/dist/pipeline/steps/xml-parse.js +27 -0
  151. package/dist/pipeline/template.d.ts +35 -0
  152. package/dist/pipeline/template.js +312 -0
  153. package/dist/rate-limiter.d.ts +56 -0
  154. package/dist/rate-limiter.js +120 -0
  155. package/dist/registry-api.d.ts +15 -0
  156. package/dist/registry-api.js +13 -0
  157. package/dist/registry.d.ts +90 -0
  158. package/dist/registry.js +100 -0
  159. package/dist/schema.d.ts +80 -0
  160. package/dist/schema.js +72 -0
  161. package/dist/spinner.d.ts +19 -0
  162. package/dist/spinner.js +37 -0
  163. package/dist/types.d.ts +101 -0
  164. package/dist/types.js +27 -0
  165. package/dist/utils.d.ts +16 -0
  166. package/dist/utils.js +40 -0
  167. package/dist/validate.d.ts +29 -0
  168. package/dist/validate.js +136 -0
  169. package/dist/verify.d.ts +20 -0
  170. package/dist/verify.js +131 -0
  171. package/dist/version.d.ts +13 -0
  172. package/dist/version.js +36 -0
  173. package/dist/xml-parser.d.ts +19 -0
  174. package/dist/xml-parser.js +119 -0
  175. package/dist/yaml-schema.d.ts +40 -0
  176. package/dist/yaml-schema.js +62 -0
  177. 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
+ }
@@ -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;