@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
package/dist/doctor.js ADDED
@@ -0,0 +1,151 @@
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
+ import chalk from 'chalk';
8
+ import { loadConfig, getConfigPath, getApiKey, getEmail } from './config.js';
9
+ import { getAllBackends } from './databases/index.js';
10
+ import { getRegistry } from './registry.js';
11
+ // ── Health-check endpoints per backend ───────────────────────────────────────
12
+ const PING_ENDPOINTS = {
13
+ ncbi: 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/einfo.fcgi?retmode=json',
14
+ uniprot: 'https://rest.uniprot.org/uniprotkb/search?query=*&size=1&format=json',
15
+ kegg: 'https://rest.kegg.jp/info/kegg',
16
+ string: 'https://string-db.org/api/json/version',
17
+ ensembl: 'https://rest.ensembl.org/info/ping?content-type=application/json',
18
+ enrichr: 'https://maayanlab.cloud/Enrichr/datasetStatistics',
19
+ };
20
+ const PING_TIMEOUT = 5000;
21
+ // ── Core checks ──────────────────────────────────────────────────────────────
22
+ function checkNodeVersion() {
23
+ const version = process.version;
24
+ const major = Number(version.slice(1).split('.')[0]);
25
+ return {
26
+ name: 'Node.js',
27
+ value: version,
28
+ ok: major >= 20,
29
+ detail: major < 20 ? 'Requires Node.js >= 20' : undefined,
30
+ };
31
+ }
32
+ function checkConfig() {
33
+ const results = [];
34
+ const configPath = getConfigPath();
35
+ const config = loadConfig();
36
+ const hasConfig = Object.keys(config).length > 0;
37
+ results.push({
38
+ name: 'Config',
39
+ value: configPath,
40
+ ok: true,
41
+ });
42
+ const apiKey = getApiKey();
43
+ if (apiKey) {
44
+ const masked = apiKey.slice(0, 4) + '****' + apiKey.slice(-4);
45
+ results.push({
46
+ name: 'API key',
47
+ value: masked,
48
+ ok: true,
49
+ detail: '10 req/s',
50
+ });
51
+ }
52
+ else {
53
+ results.push({
54
+ name: 'API key',
55
+ value: 'not set',
56
+ ok: true,
57
+ detail: '3 req/s — optional (biocli config set api_key YOUR_KEY for 10 req/s)',
58
+ });
59
+ }
60
+ const email = getEmail();
61
+ results.push({
62
+ name: 'Email',
63
+ value: email ?? 'not set',
64
+ ok: true,
65
+ detail: email ? undefined : 'Optional — recommended for NCBI (biocli config set email YOUR_EMAIL)',
66
+ });
67
+ return results;
68
+ }
69
+ async function pingBackend(name, url) {
70
+ const start = Date.now();
71
+ try {
72
+ const controller = new AbortController();
73
+ const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT);
74
+ const response = await fetch(url, { signal: controller.signal });
75
+ clearTimeout(timeout);
76
+ const elapsed = Date.now() - start;
77
+ // Consume and discard body so Node.js can close the connection promptly
78
+ await response.text().catch(() => { });
79
+ if (response.ok) {
80
+ return { name, value: url.split('/').slice(0, 3).join('/'), ok: true, detail: `${elapsed}ms` };
81
+ }
82
+ return { name, value: url.split('/').slice(0, 3).join('/'), ok: false, detail: `HTTP ${response.status} (${elapsed}ms)` };
83
+ }
84
+ catch (err) {
85
+ const elapsed = Date.now() - start;
86
+ const msg = err instanceof Error && err.name === 'AbortError'
87
+ ? `timeout (${PING_TIMEOUT}ms)`
88
+ : err instanceof Error ? err.message : String(err);
89
+ return { name, value: url.split('/').slice(0, 3).join('/'), ok: false, detail: msg };
90
+ }
91
+ }
92
+ function checkCommands() {
93
+ const count = getRegistry().size;
94
+ return { name: 'Commands', value: `${count} registered`, ok: count > 0 };
95
+ }
96
+ // ── Main runner ──────────────────────────────────────────────────────────────
97
+ export async function runDoctor() {
98
+ const checks = [];
99
+ // System checks
100
+ checks.push(checkNodeVersion());
101
+ checks.push(...checkConfig());
102
+ // Backend connectivity (parallel)
103
+ const backends = getAllBackends();
104
+ const pingPromises = backends.map(b => {
105
+ const url = PING_ENDPOINTS[b.id] ?? `${b.baseUrl}`;
106
+ return pingBackend(b.name, url);
107
+ });
108
+ const pingResults = await Promise.allSettled(pingPromises);
109
+ for (const result of pingResults) {
110
+ if (result.status === 'fulfilled') {
111
+ checks.push(result.value);
112
+ }
113
+ }
114
+ // Command registry
115
+ checks.push(checkCommands());
116
+ const allPassed = checks.every(c => c.ok);
117
+ return { checks, allPassed };
118
+ }
119
+ // ── Formatters ───────────────────────────────────────────────────────────────
120
+ export function formatDoctorText(checks, allPassed) {
121
+ const lines = ['', chalk.bold('biocli doctor'), ''];
122
+ for (const check of checks) {
123
+ const status = check.ok ? chalk.green('OK') : chalk.red('FAIL');
124
+ const detail = check.detail ? chalk.dim(` (${check.detail})`) : '';
125
+ const name = check.name.padEnd(14);
126
+ const value = check.value;
127
+ lines.push(` ${name} ${value.padEnd(44)} ${status}${detail}`);
128
+ }
129
+ const passedCount = checks.filter(c => c.ok).length;
130
+ const failedCount = checks.length - passedCount;
131
+ lines.push('');
132
+ if (allPassed) {
133
+ lines.push(chalk.green(` All ${checks.length} checks passed.`));
134
+ }
135
+ else {
136
+ lines.push(chalk.yellow(` ${passedCount} passed, ${failedCount} failed.`));
137
+ }
138
+ lines.push('');
139
+ return lines.join('\n');
140
+ }
141
+ export function formatDoctorJson(checks, allPassed) {
142
+ return JSON.stringify({
143
+ allPassed,
144
+ checks: checks.map(c => ({
145
+ name: c.name,
146
+ value: c.value,
147
+ ok: c.ok,
148
+ ...(c.detail ? { detail: c.detail } : {}),
149
+ })),
150
+ }, null, 2);
151
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Unified error types for biocli.
3
+ *
4
+ * All errors thrown by the framework should extend CliError so that
5
+ * the top-level handler can render consistent, helpful output with
6
+ * emoji-coded severity and actionable hints.
7
+ *
8
+ * ## Exit codes
9
+ *
10
+ * biocli follows Unix conventions (sysexits.h) for process exit codes:
11
+ *
12
+ * 0 Success
13
+ * 1 Generic / unexpected error
14
+ * 2 Argument / usage error (ArgumentError)
15
+ * 66 No input / empty result (EmptyResultError)
16
+ * 69 Service unavailable (AdapterLoadError)
17
+ * 75 Temporary failure, retry later (TimeoutError, RateLimitError)
18
+ * 78 Configuration error (ConfigError)
19
+ * 130 Interrupted by Ctrl-C
20
+ */
21
+ export declare const EXIT_CODES: {
22
+ readonly SUCCESS: 0;
23
+ readonly GENERIC_ERROR: 1;
24
+ readonly USAGE_ERROR: 2;
25
+ readonly EMPTY_RESULT: 66;
26
+ readonly SERVICE_UNAVAIL: 69;
27
+ readonly TEMPFAIL: 75;
28
+ readonly CONFIG_ERROR: 78;
29
+ readonly INTERRUPTED: 130;
30
+ };
31
+ export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
32
+ export declare class CliError extends Error {
33
+ /** Machine-readable error code (e.g. 'API_ERROR', 'RATE_LIMITED') */
34
+ readonly code: string;
35
+ /** Human-readable hint on how to fix the problem */
36
+ readonly hint?: string;
37
+ /** Unix process exit code — defaults to 1 (generic error) */
38
+ readonly exitCode: ExitCode;
39
+ constructor(code: string, message: string, hint?: string, exitCode?: ExitCode);
40
+ }
41
+ export declare class CommandExecutionError extends CliError {
42
+ constructor(message: string, hint?: string);
43
+ }
44
+ export declare class ConfigError extends CliError {
45
+ constructor(message: string, hint?: string);
46
+ }
47
+ export declare class TimeoutError extends CliError {
48
+ constructor(label: string, seconds: number, hint?: string);
49
+ }
50
+ export declare class ArgumentError extends CliError {
51
+ constructor(message: string, hint?: string);
52
+ }
53
+ export declare class EmptyResultError extends CliError {
54
+ constructor(command: string, hint?: string);
55
+ }
56
+ export declare class AdapterLoadError extends CliError {
57
+ constructor(message: string, hint?: string);
58
+ }
59
+ export declare class RateLimitError extends CliError {
60
+ constructor(message?: string, hint?: string);
61
+ }
62
+ export declare class ApiError extends CliError {
63
+ constructor(message: string, hint?: string);
64
+ }
65
+ /** Extract a human-readable message from an unknown caught value. */
66
+ export declare function getErrorMessage(error: unknown): string;
67
+ /** Error code -> emoji mapping for CLI output rendering. */
68
+ export declare const ERROR_ICONS: Record<string, string>;
package/dist/errors.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Unified error types for biocli.
3
+ *
4
+ * All errors thrown by the framework should extend CliError so that
5
+ * the top-level handler can render consistent, helpful output with
6
+ * emoji-coded severity and actionable hints.
7
+ *
8
+ * ## Exit codes
9
+ *
10
+ * biocli follows Unix conventions (sysexits.h) for process exit codes:
11
+ *
12
+ * 0 Success
13
+ * 1 Generic / unexpected error
14
+ * 2 Argument / usage error (ArgumentError)
15
+ * 66 No input / empty result (EmptyResultError)
16
+ * 69 Service unavailable (AdapterLoadError)
17
+ * 75 Temporary failure, retry later (TimeoutError, RateLimitError)
18
+ * 78 Configuration error (ConfigError)
19
+ * 130 Interrupted by Ctrl-C
20
+ */
21
+ // ── Exit code table ──────────────────────────────────────────────────────────
22
+ export const EXIT_CODES = {
23
+ SUCCESS: 0,
24
+ GENERIC_ERROR: 1,
25
+ USAGE_ERROR: 2, // Bad arguments / command misuse
26
+ EMPTY_RESULT: 66, // No data / not found (EX_NOINPUT)
27
+ SERVICE_UNAVAIL: 69, // Adapter load failure (EX_UNAVAILABLE)
28
+ TEMPFAIL: 75, // Timeout / rate limit (EX_TEMPFAIL)
29
+ CONFIG_ERROR: 78, // Missing / invalid config (EX_CONFIG)
30
+ INTERRUPTED: 130, // Ctrl-C / SIGINT
31
+ };
32
+ // ── Base class ───────────────────────────────────────────────────────────────
33
+ export class CliError extends Error {
34
+ /** Machine-readable error code (e.g. 'API_ERROR', 'RATE_LIMITED') */
35
+ code;
36
+ /** Human-readable hint on how to fix the problem */
37
+ hint;
38
+ /** Unix process exit code — defaults to 1 (generic error) */
39
+ exitCode;
40
+ constructor(code, message, hint, exitCode = EXIT_CODES.GENERIC_ERROR) {
41
+ super(message);
42
+ this.name = new.target.name;
43
+ this.code = code;
44
+ this.hint = hint;
45
+ this.exitCode = exitCode;
46
+ }
47
+ }
48
+ // ── Typed subclasses ─────────────────────────────────────────────────────────
49
+ export class CommandExecutionError extends CliError {
50
+ constructor(message, hint) {
51
+ super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
52
+ }
53
+ }
54
+ export class ConfigError extends CliError {
55
+ constructor(message, hint) {
56
+ super('CONFIG', message, hint, EXIT_CODES.CONFIG_ERROR);
57
+ }
58
+ }
59
+ export class TimeoutError extends CliError {
60
+ constructor(label, seconds, hint) {
61
+ super('TIMEOUT', `${label} timed out after ${seconds}s`, hint ?? 'Try again later, or increase the timeout if the NCBI server is slow', EXIT_CODES.TEMPFAIL);
62
+ }
63
+ }
64
+ export class ArgumentError extends CliError {
65
+ constructor(message, hint) {
66
+ super('ARGUMENT', message, hint, EXIT_CODES.USAGE_ERROR);
67
+ }
68
+ }
69
+ export class EmptyResultError extends CliError {
70
+ constructor(command, hint) {
71
+ super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'Check your query parameters or try a different search term', EXIT_CODES.EMPTY_RESULT);
72
+ }
73
+ }
74
+ export class AdapterLoadError extends CliError {
75
+ constructor(message, hint) {
76
+ super('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
77
+ }
78
+ }
79
+ export class RateLimitError extends CliError {
80
+ constructor(message, hint) {
81
+ super('RATE_LIMITED', message ?? 'NCBI API rate limit exceeded', hint ?? 'Add an API key (biocli config set api_key YOUR_KEY) to increase the rate limit from 3 to 10 requests/sec', EXIT_CODES.TEMPFAIL);
82
+ }
83
+ }
84
+ export class ApiError extends CliError {
85
+ constructor(message, hint) {
86
+ super('API_ERROR', message, hint ?? 'Check the NCBI API status at https://www.ncbi.nlm.nih.gov/home/develop/', EXIT_CODES.GENERIC_ERROR);
87
+ }
88
+ }
89
+ // ── Utilities ────────────────────────────────────────────────────────────────
90
+ /** Extract a human-readable message from an unknown caught value. */
91
+ export function getErrorMessage(error) {
92
+ return error instanceof Error ? error.message : String(error);
93
+ }
94
+ /** Error code -> emoji mapping for CLI output rendering. */
95
+ export const ERROR_ICONS = {
96
+ TIMEOUT: '⏱ ',
97
+ ARGUMENT: '❌',
98
+ EMPTY_RESULT: '📭',
99
+ COMMAND_EXEC: '💥',
100
+ ADAPTER_LOAD: '📦',
101
+ NETWORK: '🌐',
102
+ API_ERROR: '🚫',
103
+ RATE_LIMITED: '⏳',
104
+ CONFIG: '⚙️ ',
105
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Command execution: validates args, creates HttpContext, runs commands.
3
+ *
4
+ * This is the single entry point for executing any CLI command. It handles:
5
+ * 1. Argument validation and coercion
6
+ * 2. HttpContext creation (replaces browser sessions from opencli)
7
+ * 3. Timeout enforcement
8
+ * 4. Lazy-loading of TS modules from manifest
9
+ * 5. Lifecycle hooks (onBeforeExecute / onAfterExecute)
10
+ */
11
+ import { type CliCommand, type Arg, type CommandArgs } from './registry.js';
12
+ export declare function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): CommandArgs;
13
+ export declare function executeCommand(cmd: CliCommand, rawKwargs: CommandArgs, debug?: boolean, opts?: {
14
+ noCache?: boolean;
15
+ }): Promise<unknown>;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Command execution: validates args, creates HttpContext, runs commands.
3
+ *
4
+ * This is the single entry point for executing any CLI command. It handles:
5
+ * 1. Argument validation and coercion
6
+ * 2. HttpContext creation (replaces browser sessions from opencli)
7
+ * 3. Timeout enforcement
8
+ * 4. Lazy-loading of TS modules from manifest
9
+ * 5. Lifecycle hooks (onBeforeExecute / onAfterExecute)
10
+ */
11
+ import { getRegistry, fullName, } from './registry.js';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { executePipeline } from './pipeline/index.js';
14
+ import { AdapterLoadError, ArgumentError, CommandExecutionError, TimeoutError, getErrorMessage, } from './errors.js';
15
+ import { emitHook } from './hooks.js';
16
+ import { createHttpContextForDatabase } from './databases/index.js';
17
+ import { buildCacheKey, getCached, setCached } from './cache.js';
18
+ import { loadConfig } from './config.js';
19
+ /** Default command timeout in seconds (used when timeoutSeconds is set). */
20
+ const DEFAULT_COMMAND_TIMEOUT = 60;
21
+ const _loadedModules = new Set();
22
+ // ── Argument coercion & validation ──────────────────────────────────────────
23
+ export function coerceAndValidateArgs(cmdArgs, kwargs) {
24
+ const result = { ...kwargs };
25
+ for (const argDef of cmdArgs) {
26
+ const val = result[argDef.name];
27
+ if (argDef.required && (val === undefined || val === null || val === '')) {
28
+ throw new ArgumentError(`Argument "${argDef.name}" is required.`, argDef.help ?? `Provide a value for --${argDef.name}`);
29
+ }
30
+ if (val !== undefined && val !== null) {
31
+ if (argDef.type === 'int' || argDef.type === 'number') {
32
+ const num = Number(val);
33
+ if (Number.isNaN(num)) {
34
+ throw new ArgumentError(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
35
+ }
36
+ result[argDef.name] = num;
37
+ }
38
+ else if (argDef.type === 'boolean' || argDef.type === 'bool') {
39
+ if (typeof val === 'string') {
40
+ const lower = val.toLowerCase();
41
+ if (lower === 'true' || lower === '1')
42
+ result[argDef.name] = true;
43
+ else if (lower === 'false' || lower === '0')
44
+ result[argDef.name] = false;
45
+ else
46
+ throw new ArgumentError(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
47
+ }
48
+ else {
49
+ result[argDef.name] = Boolean(val);
50
+ }
51
+ }
52
+ const coercedVal = result[argDef.name];
53
+ if (argDef.choices && argDef.choices.length > 0) {
54
+ if (!argDef.choices.map(String).includes(String(coercedVal))) {
55
+ throw new ArgumentError(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
56
+ }
57
+ }
58
+ }
59
+ else if (argDef.default !== undefined) {
60
+ result[argDef.name] = argDef.default;
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ // ── Command runner ──────────────────────────────────────────────────────────
66
+ async function runCommand(cmd, ctx, kwargs, debug) {
67
+ const internal = cmd;
68
+ if (internal._lazy && internal._modulePath) {
69
+ const modulePath = internal._modulePath;
70
+ if (!_loadedModules.has(modulePath)) {
71
+ try {
72
+ await import(pathToFileURL(modulePath).href);
73
+ _loadedModules.add(modulePath);
74
+ }
75
+ catch (err) {
76
+ throw new AdapterLoadError(`Failed to load adapter module ${modulePath}: ${getErrorMessage(err)}`, 'Check that the adapter file exists and has no syntax errors.');
77
+ }
78
+ }
79
+ const updated = getRegistry().get(fullName(cmd));
80
+ if (updated?.func) {
81
+ return updated.func(ctx, kwargs, debug);
82
+ }
83
+ if (updated?.pipeline) {
84
+ return executePipeline(updated.pipeline, ctx, kwargs);
85
+ }
86
+ }
87
+ if (cmd.func)
88
+ return cmd.func(ctx, kwargs, debug);
89
+ if (cmd.pipeline)
90
+ return executePipeline(cmd.pipeline, ctx, kwargs);
91
+ throw new CommandExecutionError(`Command ${fullName(cmd)} has no func or pipeline`, 'This is likely a bug in the adapter definition. Please report this issue.');
92
+ }
93
+ // ── Timeout helper ──────────────────────────────────────────────────────────
94
+ async function runWithTimeout(promise, timeoutSeconds, label) {
95
+ return new Promise((resolve, reject) => {
96
+ const timer = setTimeout(() => reject(new TimeoutError(label, timeoutSeconds)), timeoutSeconds * 1000);
97
+ promise
98
+ .then((val) => { clearTimeout(timer); resolve(val); })
99
+ .catch((err) => { clearTimeout(timer); reject(err); });
100
+ });
101
+ }
102
+ // ── Required env check ──────────────────────────────────────────────────────
103
+ function ensureRequiredEnv(cmd) {
104
+ const missing = (cmd.requiredEnv ?? []).find(({ name }) => {
105
+ const value = process.env[name];
106
+ return value === undefined || value === null || value === '';
107
+ });
108
+ if (!missing)
109
+ return;
110
+ throw new CommandExecutionError(`Command ${fullName(cmd)} requires environment variable ${missing.name}.`, missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`);
111
+ }
112
+ // ── Main entry point ────────────────────────────────────────────────────────
113
+ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
114
+ let kwargs;
115
+ try {
116
+ kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
117
+ }
118
+ catch (err) {
119
+ if (err instanceof ArgumentError)
120
+ throw err;
121
+ throw new ArgumentError(getErrorMessage(err));
122
+ }
123
+ ensureRequiredEnv(cmd);
124
+ const hookCtx = {
125
+ command: fullName(cmd),
126
+ args: kwargs,
127
+ startedAt: Date.now(),
128
+ };
129
+ await emitHook('onBeforeExecute', hookCtx);
130
+ // ── Cache check ───────────────────────────────────────────────────────
131
+ const databaseId = cmd.database ?? 'ncbi';
132
+ const cacheKey = buildCacheKey(databaseId, fullName(cmd), kwargs);
133
+ const cacheConfig = loadConfig().cache;
134
+ const cacheEnabled = (cacheConfig?.enabled ?? true) && !opts.noCache;
135
+ const cacheTtlMs = (cacheConfig?.ttl ?? 24) * 60 * 60 * 1000;
136
+ if (cacheEnabled && databaseId !== 'aggregate') {
137
+ const cached = getCached(databaseId, fullName(cmd), cacheKey, cacheTtlMs);
138
+ if (cached !== null) {
139
+ if (debug)
140
+ console.error(`[Cache] HIT ${cacheKey}`);
141
+ hookCtx.finishedAt = Date.now();
142
+ await emitHook('onAfterExecute', hookCtx, cached);
143
+ return cached;
144
+ }
145
+ if (debug)
146
+ console.error(`[Cache] MISS ${cacheKey}`);
147
+ }
148
+ let result;
149
+ try {
150
+ // Aggregate commands create their own multi-database contexts internally
151
+ const ctx = databaseId === 'aggregate'
152
+ ? { databaseId: 'aggregate', fetch: async () => { throw new Error('use createHttpContextForDatabase()'); }, fetchJson: async () => { throw new Error('use createHttpContextForDatabase()'); }, fetchXml: async () => { throw new Error('use createHttpContextForDatabase()'); }, fetchText: async () => { throw new Error('use createHttpContextForDatabase()'); } }
153
+ : createHttpContextForDatabase(databaseId);
154
+ const timeout = cmd.timeoutSeconds;
155
+ if (timeout !== undefined && timeout > 0) {
156
+ result = await runWithTimeout(runCommand(cmd, ctx, kwargs, debug), timeout, fullName(cmd));
157
+ }
158
+ else {
159
+ result = await runCommand(cmd, ctx, kwargs, debug);
160
+ }
161
+ }
162
+ catch (err) {
163
+ hookCtx.error = err;
164
+ hookCtx.finishedAt = Date.now();
165
+ await emitHook('onAfterExecute', hookCtx);
166
+ throw err;
167
+ }
168
+ // ── Cache store ──────────────────────────────────────────────────────
169
+ if (cacheEnabled && databaseId !== 'aggregate' && result !== null && result !== undefined) {
170
+ try {
171
+ setCached(databaseId, fullName(cmd), cacheKey, result, cacheTtlMs);
172
+ }
173
+ catch { /* non-fatal */ }
174
+ }
175
+ hookCtx.finishedAt = Date.now();
176
+ await emitHook('onAfterExecute', hookCtx, result);
177
+ return result;
178
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Plugin lifecycle hooks: allows plugins to tap into biocli's execution lifecycle.
3
+ *
4
+ * Hooks use globalThis (like the command registry) to guarantee a single shared
5
+ * instance across all module copies — critical when TS plugins are loaded via
6
+ * npm link / peerDependency symlinks.
7
+ *
8
+ * Available hooks:
9
+ * onStartup — fired once after all commands & plugins are discovered
10
+ * onBeforeExecute — fired before every command execution
11
+ * onAfterExecute — fired after every command execution (receives result)
12
+ */
13
+ export type HookName = 'onStartup' | 'onBeforeExecute' | 'onAfterExecute';
14
+ export interface HookContext {
15
+ /** Command full name in "site/name" format, or "__startup__" for onStartup */
16
+ command: string;
17
+ /** Coerced and validated arguments */
18
+ args: Record<string, unknown>;
19
+ /** Epoch ms when execution started (set by executeCommand) */
20
+ startedAt?: number;
21
+ /** Epoch ms when execution finished (set by executeCommand) */
22
+ finishedAt?: number;
23
+ /** Error thrown by the command, if execution failed */
24
+ error?: unknown;
25
+ /** Plugins can attach arbitrary data here for cross-hook communication */
26
+ [key: string]: unknown;
27
+ }
28
+ export type HookFn = (ctx: HookContext, result?: unknown) => void | Promise<void>;
29
+ declare global {
30
+ var __biocli_hooks__: Map<HookName, HookFn[]> | undefined;
31
+ /** @deprecated Alias for __biocli_hooks__. */
32
+ var __ncbicli_hooks__: Map<HookName, HookFn[]> | undefined;
33
+ }
34
+ /** Register a hook that fires once after all plugins are discovered. */
35
+ export declare function onStartup(fn: HookFn): void;
36
+ /** Register a hook that fires before every command execution. */
37
+ export declare function onBeforeExecute(fn: HookFn): void;
38
+ /** Register a hook that fires after every command execution with the result. */
39
+ export declare function onAfterExecute(fn: HookFn): void;
40
+ /**
41
+ * Trigger all registered handlers for a hook.
42
+ * Each handler is wrapped in try/catch — a failing hook never blocks command execution.
43
+ */
44
+ export declare function emitHook(name: HookName, ctx: HookContext, result?: unknown): Promise<void>;
45
+ /**
46
+ * Remove all registered hooks. Intended for testing only.
47
+ */
48
+ export declare function clearAllHooks(): void;
package/dist/hooks.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Plugin lifecycle hooks: allows plugins to tap into biocli's execution lifecycle.
3
+ *
4
+ * Hooks use globalThis (like the command registry) to guarantee a single shared
5
+ * instance across all module copies — critical when TS plugins are loaded via
6
+ * npm link / peerDependency symlinks.
7
+ *
8
+ * Available hooks:
9
+ * onStartup — fired once after all commands & plugins are discovered
10
+ * onBeforeExecute — fired before every command execution
11
+ * onAfterExecute — fired after every command execution (receives result)
12
+ */
13
+ const _hooks = globalThis.__biocli_hooks__ ??= globalThis.__ncbicli_hooks__ ?? new Map();
14
+ globalThis.__ncbicli_hooks__ = _hooks;
15
+ // ── Registration API (used by plugins) ─────────────────────────────────────
16
+ function addHook(name, fn) {
17
+ const list = _hooks.get(name) ?? [];
18
+ if (list.includes(fn))
19
+ return;
20
+ list.push(fn);
21
+ _hooks.set(name, list);
22
+ }
23
+ /** Register a hook that fires once after all plugins are discovered. */
24
+ export function onStartup(fn) {
25
+ addHook('onStartup', fn);
26
+ }
27
+ /** Register a hook that fires before every command execution. */
28
+ export function onBeforeExecute(fn) {
29
+ addHook('onBeforeExecute', fn);
30
+ }
31
+ /** Register a hook that fires after every command execution with the result. */
32
+ export function onAfterExecute(fn) {
33
+ addHook('onAfterExecute', fn);
34
+ }
35
+ // ── Emit API (used internally by biocli core) ──────────────────────────────
36
+ /**
37
+ * Trigger all registered handlers for a hook.
38
+ * Each handler is wrapped in try/catch — a failing hook never blocks command execution.
39
+ */
40
+ export async function emitHook(name, ctx, result) {
41
+ const handlers = _hooks.get(name);
42
+ if (!handlers || handlers.length === 0)
43
+ return;
44
+ for (const fn of handlers) {
45
+ try {
46
+ await fn(ctx, result);
47
+ }
48
+ catch (err) {
49
+ console.error(`[biocli] Hook ${name} handler failed: ${err instanceof Error ? err.message : String(err)}`);
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Remove all registered hooks. Intended for testing only.
55
+ */
56
+ export function clearAllHooks() {
57
+ _hooks.clear();
58
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * biocli entry point.
4
+ *
5
+ * Discovers built-in and user CLI definitions, loads plugins,
6
+ * fires the onStartup hook, then hands off to Commander.
7
+ */
8
+ import './databases/ncbi.js';
9
+ import './databases/uniprot.js';
10
+ import './databases/kegg.js';
11
+ import './databases/string-db.js';
12
+ import './databases/ensembl.js';
13
+ import './databases/enrichr.js';
package/dist/main.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * biocli entry point.
4
+ *
5
+ * Discovers built-in and user CLI definitions, loads plugins,
6
+ * fires the onStartup hook, then hands off to Commander.
7
+ */
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+ import { discoverClis, discoverPlugins } from './discovery.js';
11
+ import { runCli } from './cli.js';
12
+ import { emitHook } from './hooks.js';
13
+ // Register database backends (side-effect imports)
14
+ import './databases/ncbi.js';
15
+ import './databases/uniprot.js';
16
+ import './databases/kegg.js';
17
+ import './databases/string-db.js';
18
+ import './databases/ensembl.js';
19
+ import './databases/enrichr.js';
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const BUILTIN_CLIS = join(__dirname, 'clis');
22
+ async function main() {
23
+ await discoverClis(BUILTIN_CLIS);
24
+ await discoverPlugins();
25
+ await emitHook('onStartup', { command: '__startup__', args: {} });
26
+ runCli();
27
+ }
28
+ main().catch((err) => {
29
+ console.error(err);
30
+ process.exit(1);
31
+ });