@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,10 @@
1
+ /**
2
+ * NCBI HTTP client — backward compatibility shim.
3
+ *
4
+ * All logic has moved to databases/ncbi.ts. This file re-exports
5
+ * everything so that existing adapter imports continue to work:
6
+ *
7
+ * import { createHttpContext } from '../ncbi-fetch.js';
8
+ * import { buildEutilsUrl } from '../ncbi-fetch.js';
9
+ */
10
+ export { EUTILS_BASE, buildEutilsUrl, ncbiFetch, createHttpContext, ncbiBackend, } from './databases/ncbi.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * NCBI HTTP client — backward compatibility shim.
3
+ *
4
+ * All logic has moved to databases/ncbi.ts. This file re-exports
5
+ * everything so that existing adapter imports continue to work:
6
+ *
7
+ * import { createHttpContext } from '../ncbi-fetch.js';
8
+ * import { buildEutilsUrl } from '../ncbi-fetch.js';
9
+ */
10
+ export { EUTILS_BASE, buildEutilsUrl, ncbiFetch, createHttpContext, ncbiBackend, } from './databases/ncbi.js';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Output formatting: table, card, JSON, Markdown, CSV, YAML, plain.
3
+ *
4
+ * Ported from opencli/src/output.ts for biocli.
5
+ */
6
+ export interface RenderOptions {
7
+ fmt?: string;
8
+ columns?: string[];
9
+ title?: string;
10
+ elapsed?: number;
11
+ source?: string;
12
+ footerExtra?: string;
13
+ /** Search query for keyword highlighting in table/plain output. */
14
+ query?: string;
15
+ /** Total result count from API (e.g. esearch count), for "3 of N" display. */
16
+ totalCount?: number;
17
+ }
18
+ export declare function render(data: unknown, opts?: RenderOptions): void;
package/dist/output.js ADDED
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Output formatting: table, card, JSON, Markdown, CSV, YAML, plain.
3
+ *
4
+ * Ported from opencli/src/output.ts for biocli.
5
+ */
6
+ import chalk from 'chalk';
7
+ import Table from 'cli-table3';
8
+ import yaml from 'js-yaml';
9
+ function normalizeRows(data) {
10
+ if (Array.isArray(data))
11
+ return data;
12
+ if (data && typeof data === 'object')
13
+ return [data];
14
+ return [{ value: data }];
15
+ }
16
+ function resolveColumns(rows, opts) {
17
+ return opts.columns ?? Object.keys(rows[0] ?? {});
18
+ }
19
+ /** Get terminal width, default to 80 if unavailable. */
20
+ function termWidth() {
21
+ return process.stdout.columns
22
+ || (process.env.COLUMNS ? parseInt(process.env.COLUMNS, 10) : 0)
23
+ || 80;
24
+ }
25
+ /** Truncate text to maxLen, appending '…' if trimmed. */
26
+ function truncateCell(text, maxLen) {
27
+ if (maxLen < 2)
28
+ return '…';
29
+ if (text.length <= maxLen)
30
+ return text;
31
+ return text.slice(0, maxLen - 1) + '…';
32
+ }
33
+ // ── Column width allocation ───────────────────────────────────────────────────
34
+ /**
35
+ * Minimum content widths for known column types.
36
+ * These are "content" widths (excluding the 2-char cell padding cli-table3 adds).
37
+ */
38
+ const COL_MIN_CONTENT = {
39
+ pmid: 8,
40
+ uid: 8,
41
+ geneid: 6,
42
+ year: 4,
43
+ date: 10,
44
+ samples: 4,
45
+ type: 4,
46
+ symbol: 6,
47
+ chromosome: 4,
48
+ };
49
+ /**
50
+ * Priority weights: higher = gets more space when distributing surplus.
51
+ * Columns not listed default to 1.
52
+ */
53
+ const COL_PRIORITY = {
54
+ title: 5,
55
+ name: 4,
56
+ abstract: 5,
57
+ summary: 4,
58
+ description: 4,
59
+ doi: 4,
60
+ journal: 3,
61
+ authors: 3,
62
+ condition: 3,
63
+ significance: 2,
64
+ organism: 2,
65
+ platform: 2,
66
+ accession: 2,
67
+ };
68
+ /**
69
+ * Allocate column widths so the table fits within terminal width.
70
+ *
71
+ * cli-table3 colWidths includes the 2-char padding (1 left + 1 right),
72
+ * so "colWidth = contentWidth + 2". Border chars (│) add (n+1) total.
73
+ *
74
+ * Total table width = sum(colWidths) + (n + 1)
75
+ */
76
+ function allocateColumnWidths(columns, rows) {
77
+ const n = columns.length;
78
+ const tw = termWidth();
79
+ const borderChars = n + 1; // │ before each column + │ at the end
80
+ const available = tw - borderChars; // total space for all colWidths
81
+ // Measure natural content width per column (max of header and all row values)
82
+ const natural = columns.map(col => {
83
+ let maxW = capitalize(col).length;
84
+ for (const row of rows) {
85
+ const v = row[col];
86
+ const len = v === null || v === undefined ? 0 : String(v).length;
87
+ if (len > maxW)
88
+ maxW = len;
89
+ }
90
+ return maxW + 2; // +2 for cell padding → this is the colWidth value
91
+ });
92
+ const totalNatural = natural.reduce((a, b) => a + b, 0);
93
+ // If everything fits naturally, use natural widths
94
+ if (totalNatural <= available)
95
+ return natural;
96
+ // Otherwise, compute minimum widths and distribute remaining space by priority
97
+ const mins = columns.map(col => {
98
+ const headerW = capitalize(col).length;
99
+ const knownMin = COL_MIN_CONTENT[col.toLowerCase()];
100
+ const contentMin = knownMin ?? Math.min(headerW, 8);
101
+ return Math.max(contentMin, headerW) + 2; // +2 for padding
102
+ });
103
+ const totalMins = mins.reduce((a, b) => a + b, 0);
104
+ if (totalMins >= available) {
105
+ // Even minimums don't fit — proportionally shrink minimums
106
+ const ratio = available / totalMins;
107
+ return mins.map(m => Math.max(4, Math.floor(m * ratio)));
108
+ }
109
+ // Distribute surplus space proportionally by priority × natural demand
110
+ const surplus = available - totalMins;
111
+ const wants = columns.map((col, i) => {
112
+ const priority = COL_PRIORITY[col.toLowerCase()] ?? 1;
113
+ const extra = Math.max(0, natural[i] - mins[i]);
114
+ return priority * extra;
115
+ });
116
+ const totalWant = wants.reduce((a, b) => a + b, 0);
117
+ if (totalWant === 0)
118
+ return mins;
119
+ return mins.map((min, i) => {
120
+ const share = Math.floor(surplus * wants[i] / totalWant);
121
+ // Never exceed the natural width (no point adding empty space)
122
+ return Math.min(min + share, natural[i]);
123
+ });
124
+ }
125
+ // ── Terminal hyperlinks (OSC 8) ───────────────────────────────────────────────
126
+ /** Whether the terminal supports OSC 8 hyperlinks. */
127
+ const HYPERLINKS_SUPPORTED = !!(process.stdout.isTTY && process.env.TERM_PROGRAM !== 'Apple_Terminal');
128
+ /** Wrap text in an OSC 8 clickable hyperlink (no-op if unsupported). */
129
+ function hyperlink(text, url) {
130
+ if (!HYPERLINKS_SUPPORTED)
131
+ return text;
132
+ return `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007`;
133
+ }
134
+ /**
135
+ * Known column names → URL patterns for automatic hyperlinking.
136
+ * Returns the URL if the value can be linked, undefined otherwise.
137
+ */
138
+ function autoLinkUrl(column, value) {
139
+ const col = column.toLowerCase();
140
+ if (col === 'doi' && value && !value.startsWith('http')) {
141
+ return `https://doi.org/${value}`;
142
+ }
143
+ if (col === 'pmid' && /^\d+$/.test(value)) {
144
+ return `https://pubmed.ncbi.nlm.nih.gov/${value}/`;
145
+ }
146
+ if (col === 'geneid' && /^\d+$/.test(value)) {
147
+ return `https://www.ncbi.nlm.nih.gov/gene/${value}`;
148
+ }
149
+ if (col === 'accession') {
150
+ if (/^GSE\d+$/i.test(value))
151
+ return `https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=${value}`;
152
+ if (/^SRR\d+$/i.test(value))
153
+ return `https://www.ncbi.nlm.nih.gov/sra/${value}`;
154
+ if (/^RCV\d+$/i.test(value))
155
+ return `https://www.ncbi.nlm.nih.gov/clinvar/${value}/`;
156
+ }
157
+ if (col === 'uid' && /^\d+$/.test(value)) {
158
+ // ClinVar UID
159
+ return `https://www.ncbi.nlm.nih.gov/clinvar/variation/${value}/`;
160
+ }
161
+ return undefined;
162
+ }
163
+ /** Apply hyperlink to a cell value if the column is linkable. */
164
+ function linkCell(column, text) {
165
+ const url = autoLinkUrl(column, text);
166
+ return url ? hyperlink(text, url) : text;
167
+ }
168
+ /** Highlight query keywords in text using chalk.bold.underline. */
169
+ function highlightQuery(text, query) {
170
+ if (!query || !text)
171
+ return text;
172
+ // Split query into individual words, escape regex chars
173
+ const words = query.split(/\s+/).filter(w => w.length > 1);
174
+ if (!words.length)
175
+ return text;
176
+ const escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
177
+ const re = new RegExp(`(${escaped.join('|')})`, 'gi');
178
+ return text.replace(re, (match) => chalk.bold.underline(match));
179
+ }
180
+ export function render(data, opts = {}) {
181
+ const fmt = opts.fmt ?? 'table';
182
+ if (data === null || data === undefined) {
183
+ console.log(data);
184
+ return;
185
+ }
186
+ switch (fmt) {
187
+ case 'json':
188
+ renderJson(data);
189
+ break;
190
+ case 'plain':
191
+ renderPlain(data, opts);
192
+ break;
193
+ case 'md':
194
+ case 'markdown':
195
+ renderMarkdown(data, opts);
196
+ break;
197
+ case 'csv':
198
+ renderCsv(data, opts);
199
+ break;
200
+ case 'yaml':
201
+ case 'yml':
202
+ renderYaml(data);
203
+ break;
204
+ default:
205
+ renderTable(data, opts);
206
+ break;
207
+ }
208
+ }
209
+ // ── Card view for single records ──────────────────────────────────────────────
210
+ function renderCard(row, columns, opts) {
211
+ const tw = termWidth();
212
+ const labelWidth = Math.max(...columns.map(c => capitalize(c).length)) + 2;
213
+ const valueWidth = tw - labelWidth - 6; // padding and borders
214
+ console.log();
215
+ console.log(chalk.dim(` ── ${opts.title ?? 'result'} ${'─'.repeat(Math.max(0, tw - (opts.title?.length ?? 6) - 8))}`));
216
+ for (const col of columns) {
217
+ const raw = row[col];
218
+ const value = raw === null || raw === undefined ? '' : String(raw);
219
+ const label = capitalize(col);
220
+ if (!value)
221
+ continue;
222
+ // Long text fields get their own block
223
+ if (value.length > valueWidth) {
224
+ console.log(chalk.bold.cyan(` ${label}`));
225
+ // Word-wrap the value
226
+ const wrapped = wordWrap(highlightQuery(value, opts.query), tw - 4);
227
+ for (const line of wrapped) {
228
+ console.log(` ${line}`);
229
+ }
230
+ }
231
+ else {
232
+ const paddedLabel = label.padEnd(labelWidth);
233
+ const displayed = linkCell(col, highlightQuery(value, opts.query));
234
+ console.log(` ${chalk.bold.cyan(paddedLabel)}${displayed}`);
235
+ }
236
+ }
237
+ // Footer
238
+ const footer = [];
239
+ if (opts.elapsed)
240
+ footer.push(`${opts.elapsed.toFixed(1)}s`);
241
+ if (opts.source)
242
+ footer.push(opts.source);
243
+ if (opts.footerExtra)
244
+ footer.push(opts.footerExtra);
245
+ console.log(chalk.dim(` ${'─'.repeat(Math.max(0, tw - 4))}${footer.length ? '\n ' + footer.join(' · ') : ''}`));
246
+ }
247
+ /** Simple word-wrap: break text into lines of at most `width` characters. */
248
+ function wordWrap(text, width) {
249
+ if (text.length <= width)
250
+ return [text];
251
+ const lines = [];
252
+ let remaining = text;
253
+ while (remaining.length > 0) {
254
+ if (remaining.length <= width) {
255
+ lines.push(remaining);
256
+ break;
257
+ }
258
+ // Find last space within width
259
+ let breakAt = remaining.lastIndexOf(' ', width);
260
+ if (breakAt <= 0)
261
+ breakAt = width;
262
+ lines.push(remaining.slice(0, breakAt));
263
+ remaining = remaining.slice(breakAt).trimStart();
264
+ }
265
+ return lines;
266
+ }
267
+ // ── Table view ────────────────────────────────────────────────────────────────
268
+ function renderTable(data, opts) {
269
+ const rows = normalizeRows(data);
270
+ if (!rows.length) {
271
+ console.log();
272
+ console.log(chalk.yellow(' No results found'));
273
+ if (opts.query) {
274
+ console.log(chalk.dim(` Try a broader search term or check spelling`));
275
+ }
276
+ return;
277
+ }
278
+ const columns = resolveColumns(rows, opts);
279
+ // Single record → card view (much more readable than a 1-row wide table)
280
+ if (rows.length === 1) {
281
+ renderCard(rows[0], columns, opts);
282
+ return;
283
+ }
284
+ // Compute column widths that fit within the terminal
285
+ const colWidths = allocateColumnWidths(columns, rows);
286
+ const header = columns.map(c => capitalize(c));
287
+ const table = new Table({
288
+ head: header.map(h => chalk.bold(h)),
289
+ style: { head: [], border: [] },
290
+ colWidths,
291
+ });
292
+ for (const row of rows) {
293
+ table.push(columns.map((c, i) => {
294
+ const v = row[c];
295
+ if (v === null || v === undefined)
296
+ return '';
297
+ let text = String(v);
298
+ // Truncate plain text to fit column content width FIRST,
299
+ // then apply ANSI formatting. This avoids breaking escape sequences
300
+ // and prevents invisible bytes from inflating width.
301
+ const contentW = colWidths[i] - 2; // subtract cell padding
302
+ text = truncateCell(text, contentW);
303
+ // Apply highlighting on truncated plain text (safe — chalk codes
304
+ // are handled by cli-table3's strip-ansi for width calculation).
305
+ // NOTE: No OSC 8 hyperlinks in table cells — they break width calc.
306
+ return highlightQuery(text, opts.query);
307
+ }));
308
+ }
309
+ console.log();
310
+ if (opts.title)
311
+ console.log(chalk.dim(` ${opts.title}`));
312
+ console.log(table.toString());
313
+ renderFooter(rows.length, opts);
314
+ }
315
+ // ── Footer helper ─────────────────────────────────────────────────────────────
316
+ function renderFooter(count, opts) {
317
+ const footer = [];
318
+ const hasMore = opts.totalCount && opts.totalCount > count;
319
+ if (hasMore) {
320
+ footer.push(`${count} of ${opts.totalCount.toLocaleString()} items`);
321
+ }
322
+ else {
323
+ footer.push(`${count} items`);
324
+ }
325
+ if (opts.elapsed)
326
+ footer.push(`${opts.elapsed.toFixed(1)}s`);
327
+ if (opts.source)
328
+ footer.push(opts.source);
329
+ if (opts.footerExtra)
330
+ footer.push(opts.footerExtra);
331
+ console.log(chalk.dim(footer.join(' · ')));
332
+ if (hasMore) {
333
+ console.log(chalk.dim(` Use --limit <n> to show more results`));
334
+ }
335
+ }
336
+ function renderJson(data) {
337
+ console.log(JSON.stringify(data, null, 2));
338
+ }
339
+ function renderPlain(data, opts) {
340
+ const rows = normalizeRows(data);
341
+ if (!rows.length)
342
+ return;
343
+ // Single-row single-field shortcuts for simple commands.
344
+ if (rows.length === 1) {
345
+ const row = rows[0];
346
+ const entries = Object.entries(row);
347
+ if (entries.length === 1) {
348
+ const [key, value] = entries[0];
349
+ if (key === 'response' || key === 'content' || key === 'text' || key === 'value') {
350
+ console.log(highlightQuery(String(value ?? ''), opts.query));
351
+ return;
352
+ }
353
+ }
354
+ }
355
+ rows.forEach((row, index) => {
356
+ const entries = Object.entries(row).filter(([, value]) => value !== undefined && value !== null && String(value) !== '');
357
+ entries.forEach(([key, value]) => {
358
+ console.log(`${key}: ${highlightQuery(String(value), opts.query)}`);
359
+ });
360
+ if (index < rows.length - 1)
361
+ console.log('');
362
+ });
363
+ }
364
+ function renderMarkdown(data, opts) {
365
+ const rows = normalizeRows(data);
366
+ if (!rows.length)
367
+ return;
368
+ const columns = resolveColumns(rows, opts);
369
+ console.log('| ' + columns.join(' | ') + ' |');
370
+ console.log('| ' + columns.map(() => '---').join(' | ') + ' |');
371
+ for (const row of rows) {
372
+ console.log('| ' + columns.map(c => String(row[c] ?? '')).join(' | ') + ' |');
373
+ }
374
+ }
375
+ function renderCsv(data, opts) {
376
+ const rows = normalizeRows(data);
377
+ if (!rows.length)
378
+ return;
379
+ const columns = resolveColumns(rows, opts);
380
+ console.log(columns.join(','));
381
+ for (const row of rows) {
382
+ console.log(columns.map(c => {
383
+ const v = String(row[c] ?? '');
384
+ return v.includes(',') || v.includes('"') || v.includes('\n') || v.includes('\r')
385
+ ? `"${v.replace(/"/g, '""')}"` : v;
386
+ }).join(','));
387
+ }
388
+ }
389
+ function renderYaml(data) {
390
+ console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
391
+ }
392
+ function capitalize(s) {
393
+ return s.charAt(0).toUpperCase() + s.slice(1);
394
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pipeline executor: runs YAML pipeline steps sequentially.
3
+ *
4
+ * Adapted from opencli:
5
+ * - No browser retry logic
6
+ * - No browser window close on failure
7
+ * - Sequential step execution with getStepHandler() from registry
8
+ */
9
+ import type { HttpContext } from '../types.js';
10
+ /**
11
+ * Execute a pipeline (array of step objects) sequentially.
12
+ *
13
+ * Each step object has a single key (the step name) whose value is the params.
14
+ * Data flows from one step to the next: the return value of step N becomes
15
+ * the `data` argument for step N+1.
16
+ *
17
+ * @param pipeline - Array of step objects, e.g. [{ fetch: '...' }, { select: 'result.items' }]
18
+ * @param ctx - HttpContext for NCBI API access (may be null for pure transforms)
19
+ * @param args - User-provided arguments from the command invocation
20
+ * @returns The final data value after all steps have executed
21
+ */
22
+ export declare function executePipeline(pipeline: Record<string, unknown>[], ctx: HttpContext | null, args: Record<string, unknown>): Promise<unknown>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Pipeline executor: runs YAML pipeline steps sequentially.
3
+ *
4
+ * Adapted from opencli:
5
+ * - No browser retry logic
6
+ * - No browser window close on failure
7
+ * - Sequential step execution with getStepHandler() from registry
8
+ */
9
+ import { getStepHandler } from './registry.js';
10
+ /**
11
+ * Execute a pipeline (array of step objects) sequentially.
12
+ *
13
+ * Each step object has a single key (the step name) whose value is the params.
14
+ * Data flows from one step to the next: the return value of step N becomes
15
+ * the `data` argument for step N+1.
16
+ *
17
+ * @param pipeline - Array of step objects, e.g. [{ fetch: '...' }, { select: 'result.items' }]
18
+ * @param ctx - HttpContext for NCBI API access (may be null for pure transforms)
19
+ * @param args - User-provided arguments from the command invocation
20
+ * @returns The final data value after all steps have executed
21
+ */
22
+ export async function executePipeline(pipeline, ctx, args) {
23
+ let data = undefined;
24
+ for (let i = 0; i < pipeline.length; i++) {
25
+ const step = pipeline[i];
26
+ if (!step || typeof step !== 'object')
27
+ continue;
28
+ const entries = Object.entries(step);
29
+ if (entries.length === 0)
30
+ continue;
31
+ const [stepName, params] = entries[0];
32
+ const handler = getStepHandler(stepName);
33
+ if (!handler) {
34
+ throw new Error(`Unknown pipeline step "${stepName}" at index ${i}. ` +
35
+ `Known steps: ${(await import('./registry.js')).getKnownStepNames().join(', ')}`);
36
+ }
37
+ data = await handler(ctx, params, data, args);
38
+ }
39
+ return data;
40
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pipeline engine — public API re-exports.
3
+ */
4
+ export { executePipeline } from './executor.js';
5
+ export { getStepHandler, getKnownStepNames } from './registry.js';
6
+ export { renderTemplate, renderValue } from './template.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pipeline engine — public API re-exports.
3
+ */
4
+ export { executePipeline } from './executor.js';
5
+ export { getStepHandler, getKnownStepNames } from './registry.js';
6
+ export { renderTemplate, renderValue } from './template.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Dynamic registry for pipeline steps.
3
+ *
4
+ * Only data pipeline steps are registered (no browser steps).
5
+ * Allows core and third-party plugins to register custom YAML operations.
6
+ */
7
+ import type { HttpContext } from '../types.js';
8
+ export type StepHandler = (ctx: HttpContext | null, params: unknown, data: unknown, args: Record<string, unknown>) => Promise<unknown>;
9
+ /**
10
+ * Get a registered step handler by name.
11
+ */
12
+ export declare function getStepHandler(name: string): StepHandler | undefined;
13
+ /**
14
+ * List all registered step names (useful for validation / help output).
15
+ */
16
+ export declare function getKnownStepNames(): string[];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Dynamic registry for pipeline steps.
3
+ *
4
+ * Only data pipeline steps are registered (no browser steps).
5
+ * Allows core and third-party plugins to register custom YAML operations.
6
+ */
7
+ import { handleFetch } from './steps/fetch.js';
8
+ import { handleSelect, handleMap, handleFilter, handleSort, handleLimit, } from './steps/transform.js';
9
+ import { handleXmlParse } from './steps/xml-parse.js';
10
+ const stepRegistry = new Map();
11
+ // ── Register core steps ─────────────────────────────────────────────────────
12
+ stepRegistry.set('fetch', handleFetch);
13
+ stepRegistry.set('select', handleSelect);
14
+ stepRegistry.set('map', handleMap);
15
+ stepRegistry.set('filter', handleFilter);
16
+ stepRegistry.set('sort', handleSort);
17
+ stepRegistry.set('limit', handleLimit);
18
+ stepRegistry.set('xml-parse', handleXmlParse);
19
+ // ── Public API ──────────────────────────────────────────────────────────────
20
+ /**
21
+ * Get a registered step handler by name.
22
+ */
23
+ export function getStepHandler(name) {
24
+ return stepRegistry.get(name);
25
+ }
26
+ /**
27
+ * List all registered step names (useful for validation / help output).
28
+ */
29
+ export function getKnownStepNames() {
30
+ return [...stepRegistry.keys()];
31
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Pipeline step: fetch — HTTP API requests for NCBI endpoints.
3
+ *
4
+ * Adapted from opencli:
5
+ * - No browser-related fetch paths (no page.evaluate, no fetchBatchInBrowser)
6
+ * - Always uses Node.js built-in fetch()
7
+ * - Automatic API key / email injection via HttpContext
8
+ * - Rate limiting integration via HttpContext.fetch() or standalone rate limiter
9
+ * - XML auto-detection: if Content-Type contains 'xml', auto-parse with parseXml()
10
+ * - Per-item fetch pattern (when data is array and URL contains `item`)
11
+ * - Configurable concurrency (default 5)
12
+ */
13
+ import type { HttpContext } from '../../types.js';
14
+ /**
15
+ * Pipeline fetch step handler.
16
+ *
17
+ * Params can be:
18
+ * - A string (URL template)
19
+ * - An object with: url, method, params, headers, concurrency
20
+ */
21
+ export declare function handleFetch(ctx: HttpContext | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown>;