@yeyuan98/opencode-bioresearcher-plugin 1.7.0 → 1.7.1

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.
@@ -0,0 +1,68 @@
1
+ export interface SheetInfo {
2
+ name: string;
3
+ index: number;
4
+ }
5
+ export interface ITableBackend {
6
+ readonly type: string;
7
+ listSheets(): SheetInfo[];
8
+ getHeaders(sheetIndex: number): {
9
+ headers: string[];
10
+ columnCount: number;
11
+ };
12
+ getTotalDataRows(sheetIndex: number): number;
13
+ getCell(sheetIndex: number, cellAddress: string): {
14
+ value: any;
15
+ type: string;
16
+ };
17
+ getRange(sheetIndex: number, range: string): {
18
+ data: any[][];
19
+ rows: number;
20
+ columns: number;
21
+ clamped_to?: string;
22
+ out_of_bounds?: boolean;
23
+ };
24
+ getPreview(sheetIndex: number, maxRows: number): {
25
+ rows: any[][];
26
+ totalRows: number;
27
+ };
28
+ search(sheetIndex: number, searchTerm: string, options: {
29
+ column?: string;
30
+ maxResults: number;
31
+ maxRowsScan: number;
32
+ }): {
33
+ results: any[];
34
+ totalMatchesFound: number;
35
+ rowsScanned: number;
36
+ };
37
+ filterRows(sheetIndex: number, column: string, operator: string, value: any, options: {
38
+ maxResults: number;
39
+ maxRowsScan: number;
40
+ }): {
41
+ matchedRows: Record<string, any>[];
42
+ totalMatchesFound: number;
43
+ totalDataRows: number;
44
+ };
45
+ summarize(sheetIndex: number, columns: string[] | undefined, options: {
46
+ maxRowsScan: number;
47
+ }): {
48
+ summaries: Record<string, any>;
49
+ rowCount: number;
50
+ };
51
+ groupBy(sheetIndex: number, groupColumn: string, aggColumn: string, aggType: string, options: {
52
+ maxRowsScan: number;
53
+ }): {
54
+ groups: Record<string, number>;
55
+ };
56
+ pivotSummary(sheetIndex: number, rowField: string, colField: string, valueField: string, agg: string, options: {
57
+ maxRowsScan: number;
58
+ }): {
59
+ columns: string[];
60
+ pivot: Record<string, Record<string, number>>;
61
+ };
62
+ dispose(): void;
63
+ }
64
+ export interface IWritableTableBackend extends ITableBackend {
65
+ appendRows(sheetIndex: number, rows: any[]): void;
66
+ updateCell(sheetIndex: number, cellAddress: string, value: any): void;
67
+ createFile(sheetName: string, data: any): void;
68
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import { BaseTableBackend } from '../base';
2
+ import type { IWritableTableBackend, SheetInfo } from '../interface';
3
+ export declare class XLSXBackend extends BaseTableBackend implements IWritableTableBackend {
4
+ readonly type = "xlsx";
5
+ private workbook;
6
+ private sheetMap;
7
+ constructor(resolvedPath: string, force: boolean);
8
+ private getSheetData;
9
+ listSheets(): SheetInfo[];
10
+ getHeaders(sheetIndex: number): {
11
+ headers: string[];
12
+ columnCount: number;
13
+ };
14
+ getTotalDataRows(sheetIndex: number): number;
15
+ getCell(sheetIndex: number, cellAddress: string): {
16
+ value: any;
17
+ type: any;
18
+ };
19
+ getRange(sheetIndex: number, range: string): any;
20
+ getPreview(sheetIndex: number, maxRows: number): {
21
+ rows: any[][];
22
+ totalRows: number;
23
+ };
24
+ search(sheetIndex: number, searchTerm: string, options: {
25
+ column?: string;
26
+ maxResults: number;
27
+ maxRowsScan: number;
28
+ }): {
29
+ results: any[];
30
+ totalMatchesFound: number;
31
+ rowsScanned: number;
32
+ };
33
+ filterRows(sheetIndex: number, column: string, operator: string, value: any, options: {
34
+ maxResults: number;
35
+ maxRowsScan: number;
36
+ }): {
37
+ matchedRows: Record<string, any>[];
38
+ totalMatchesFound: number;
39
+ totalDataRows: number;
40
+ };
41
+ summarize(sheetIndex: number, columns: string[] | undefined, options: {
42
+ maxRowsScan: number;
43
+ }): {
44
+ summaries: Record<string, any>;
45
+ rowCount: number;
46
+ };
47
+ groupBy(sheetIndex: number, groupColumn: string, aggColumn: string, aggType: string, options: {
48
+ maxRowsScan: number;
49
+ }): {
50
+ groups: Record<string, number>;
51
+ };
52
+ pivotSummary(sheetIndex: number, rowField: string, colField: string, valueField: string, agg: string, options: {
53
+ maxRowsScan: number;
54
+ }): {
55
+ columns: string[];
56
+ pivot: Record<string, Record<string, number>>;
57
+ };
58
+ appendRows(sheetIndex: number, rows: any[]): void;
59
+ updateCell(sheetIndex: number, cellAddress: string, value: any): void;
60
+ createFile(sheetName: string, data: any): void;
61
+ dispose(): void;
62
+ }
@@ -0,0 +1,547 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as crypto from 'crypto';
4
+ import * as XLSX from '../../../../vendor/xlsx.mjs';
5
+ XLSX.set_fs(fs);
6
+ import { BaseTableBackend } from '../base';
7
+ import { iterativeMin, iterativeMax, DEFAULT_MAX_RANGE_CELLS } from '../../utils';
8
+ const workbookCache = new Map();
9
+ const MAX_CACHE_ENTRIES = 3;
10
+ function getOrCacheWorkbook(resolvedPath, force) {
11
+ const stat = fs.statSync(resolvedPath);
12
+ const cached = workbookCache.get(resolvedPath);
13
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
14
+ return cached.workbook;
15
+ }
16
+ const workbook = XLSX.readFile(resolvedPath);
17
+ workbookCache.set(resolvedPath, { workbook, mtimeMs: stat.mtimeMs });
18
+ if (workbookCache.size > MAX_CACHE_ENTRIES) {
19
+ const oldest = workbookCache.keys().next().value;
20
+ if (oldest !== undefined)
21
+ workbookCache.delete(oldest);
22
+ }
23
+ return workbook;
24
+ }
25
+ function invalidateCache(resolvedPath) {
26
+ workbookCache.delete(resolvedPath);
27
+ }
28
+ function convertExcelDate(excelDate) {
29
+ const daysSinceEpoch = excelDate - 25569;
30
+ const timestamp = daysSinceEpoch * 86400 * 1000;
31
+ const date = new Date(timestamp);
32
+ return date.toISOString().split('T')[0];
33
+ }
34
+ function processCellValue(cell) {
35
+ if (!cell)
36
+ return "";
37
+ switch (cell.t) {
38
+ case 'd': return convertExcelDate(cell.v);
39
+ case 'n': return cell.v;
40
+ case 'b': return cell.v;
41
+ case 's': return cell.v;
42
+ case 'e': return cell.w || cell.v;
43
+ case 'z': return "";
44
+ default: return cell.w || cell.v || "";
45
+ }
46
+ }
47
+ function getSheetDimensions(worksheet) {
48
+ const ref = worksheet['!ref'] ? XLSX.utils.decode_range(worksheet['!ref']) : null;
49
+ return ref
50
+ ? { startRow: ref.s.r, endRow: ref.e.r, startCol: ref.s.c, endCol: ref.e.c }
51
+ : { startRow: 0, endRow: 0, startCol: 0, endCol: 0 };
52
+ }
53
+ function deduplicateHeaders(rawHeaders) {
54
+ const headers = [];
55
+ const seen = new Map();
56
+ for (let i = 0; i < rawHeaders.length; i++) {
57
+ const raw = rawHeaders[i];
58
+ const count = seen.get(raw) ?? 0;
59
+ seen.set(raw, count + 1);
60
+ headers.push(count > 0 ? `${raw}_${count}` : raw);
61
+ }
62
+ return headers;
63
+ }
64
+ function getHeadersFromSheet(worksheet, dims) {
65
+ const rawHeaders = [];
66
+ for (let C = dims.startCol; C <= dims.endCol; C++) {
67
+ const addr = XLSX.utils.encode_cell({ r: dims.startRow, c: C });
68
+ const processed = processCellValue(worksheet[addr]);
69
+ const raw = (processed !== undefined && processed !== null && processed !== '') ? String(processed) : `Column_${C}`;
70
+ rawHeaders.push(raw);
71
+ }
72
+ return deduplicateHeaders(rawHeaders);
73
+ }
74
+ function rowToObject(worksheet, rowIdx, headers, startCol, endCol) {
75
+ const obj = {};
76
+ for (let C = startCol; C <= endCol; C++) {
77
+ const addr = XLSX.utils.encode_cell({ r: rowIdx, c: C });
78
+ const cell = worksheet[addr];
79
+ if (cell && cell.v !== undefined && cell.v !== null) {
80
+ obj[headers[C - startCol]] = cell.v;
81
+ }
82
+ }
83
+ return obj;
84
+ }
85
+ function getCellValue(worksheet, row, col) {
86
+ const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })];
87
+ return processCellValue(cell);
88
+ }
89
+ function writeAtomic(workbook, filePath) {
90
+ const dir = path.dirname(filePath);
91
+ const suffix = crypto.randomBytes(8).toString('hex');
92
+ const tmpPath = path.join(dir, `.tmp_${suffix}_${path.basename(filePath)}`);
93
+ try {
94
+ XLSX.writeFile(workbook, tmpPath);
95
+ fs.renameSync(tmpPath, filePath);
96
+ invalidateCache(filePath);
97
+ }
98
+ catch (e) {
99
+ if (fs.existsSync(tmpPath)) {
100
+ fs.unlinkSync(tmpPath);
101
+ }
102
+ throw e;
103
+ }
104
+ }
105
+ const CELL_ADDRESS_REGEX = /^[A-Za-z]+\d+$/;
106
+ export class XLSXBackend extends BaseTableBackend {
107
+ type = 'xlsx';
108
+ workbook;
109
+ sheetMap;
110
+ constructor(resolvedPath, force) {
111
+ super(resolvedPath, force);
112
+ this.workbook = getOrCacheWorkbook(resolvedPath, force);
113
+ this.sheetMap = new Map();
114
+ }
115
+ getSheetData(sheetIndex) {
116
+ const sheetName = this.workbook.SheetNames[sheetIndex];
117
+ if (!sheetName)
118
+ throw new Error(`Sheet index ${sheetIndex} out of range`);
119
+ if (!this.sheetMap.has(sheetName)) {
120
+ const worksheet = this.workbook.Sheets[sheetName];
121
+ const dims = getSheetDimensions(worksheet);
122
+ const headers = getHeadersFromSheet(worksheet, dims);
123
+ this.sheetMap.set(sheetName, { worksheet, dims, headers });
124
+ }
125
+ return this.sheetMap.get(sheetName);
126
+ }
127
+ listSheets() {
128
+ return this.workbook.SheetNames.map((name, i) => ({ name, index: i }));
129
+ }
130
+ getHeaders(sheetIndex) {
131
+ const sd = this.getSheetData(sheetIndex);
132
+ return { headers: sd.headers, columnCount: sd.headers.length };
133
+ }
134
+ getTotalDataRows(sheetIndex) {
135
+ const sd = this.getSheetData(sheetIndex);
136
+ return sd.dims.endRow - sd.dims.startRow;
137
+ }
138
+ getCell(sheetIndex, cellAddress) {
139
+ if (!CELL_ADDRESS_REGEX.test(cellAddress)) {
140
+ throw new Error(`Invalid cell address "${cellAddress}". Expected format like "A1", "B5", "AA123".`);
141
+ }
142
+ const sd = this.getSheetData(sheetIndex);
143
+ const cell = sd.worksheet[cellAddress];
144
+ const value = processCellValue(cell);
145
+ return { value, type: cell?.t || 'unknown' };
146
+ }
147
+ getRange(sheetIndex, range) {
148
+ const sd = this.getSheetData(sheetIndex);
149
+ const decoded = XLSX.utils.decode_range(range);
150
+ const requestedCells = (decoded.e.r - decoded.s.r + 1) * (decoded.e.c - decoded.s.c + 1);
151
+ if (!this.force && requestedCells > DEFAULT_MAX_RANGE_CELLS) {
152
+ throw new Error(`Range "${range}" exceeds ${DEFAULT_MAX_RANGE_CELLS} cell limit (${requestedCells} cells). Set force: true to override.`);
153
+ }
154
+ const sheetRef = sd.worksheet['!ref'] ? XLSX.utils.decode_range(sd.worksheet['!ref']) : null;
155
+ if (!sheetRef) {
156
+ return { data: [], rows: 0, columns: 0, out_of_bounds: true };
157
+ }
158
+ const clamped = {
159
+ s: { r: Math.max(decoded.s.r, sheetRef.s.r), c: Math.max(decoded.s.c, sheetRef.s.c) },
160
+ e: { r: Math.min(decoded.e.r, sheetRef.e.r), c: Math.min(decoded.e.c, sheetRef.e.c) },
161
+ };
162
+ if (clamped.s.r > clamped.e.r || clamped.s.c > clamped.e.c) {
163
+ return { data: [], rows: 0, columns: 0, out_of_bounds: true };
164
+ }
165
+ const data = [];
166
+ for (let R = clamped.s.r; R <= clamped.e.r; ++R) {
167
+ const row = [];
168
+ for (let C = clamped.s.c; C <= clamped.e.c; ++C) {
169
+ row.push(getCellValue(sd.worksheet, R, C));
170
+ }
171
+ data.push(row);
172
+ }
173
+ const result = {
174
+ data,
175
+ rows: data.length,
176
+ columns: data[0]?.length || 0,
177
+ };
178
+ if (clamped.s.r !== decoded.s.r || clamped.s.c !== decoded.s.c || clamped.e.r !== decoded.e.r || clamped.e.c !== decoded.e.c) {
179
+ result.clamped_to = XLSX.utils.encode_range(clamped);
180
+ }
181
+ return result;
182
+ }
183
+ getPreview(sheetIndex, maxRows) {
184
+ const sd = this.getSheetData(sheetIndex);
185
+ const previewRows = [];
186
+ for (let R = sd.dims.startRow; R <= sd.dims.endRow && previewRows.length < maxRows; R++) {
187
+ const row = [];
188
+ for (let C = sd.dims.startCol; C <= sd.dims.endCol; C++) {
189
+ row.push(getCellValue(sd.worksheet, R, C));
190
+ }
191
+ previewRows.push(row);
192
+ }
193
+ return { rows: previewRows, totalRows: sd.dims.endRow - sd.dims.startRow + 1 };
194
+ }
195
+ search(sheetIndex, searchTerm, options) {
196
+ const sd = this.getSheetData(sheetIndex);
197
+ const results = [];
198
+ let totalMatchesFound = 0;
199
+ let rowsScanned = 0;
200
+ const maxScanRow = Math.min(sd.dims.startRow + options.maxRowsScan, sd.dims.endRow);
201
+ if (options.column) {
202
+ const colIdx = sd.headers.indexOf(options.column);
203
+ if (colIdx === -1) {
204
+ throw new Error(`Column "${options.column}" not found. Available columns: ${sd.headers.slice(0, 20).join(', ')}${sd.headers.length > 20 ? ` ... (${sd.headers.length} total)` : ''}`);
205
+ }
206
+ const absCol = sd.dims.startCol + colIdx;
207
+ for (let R = sd.dims.startRow; R <= maxScanRow; ++R) {
208
+ rowsScanned++;
209
+ const cell = sd.worksheet[XLSX.utils.encode_cell({ r: R, c: absCol })];
210
+ if (cell && cell.v !== undefined) {
211
+ const cellValue = String(processCellValue(cell)).toLowerCase();
212
+ if (cellValue.includes(searchTerm.toLowerCase())) {
213
+ totalMatchesFound++;
214
+ if (results.length < options.maxResults) {
215
+ const address = XLSX.utils.encode_cell({ r: R, c: absCol });
216
+ results.push({ address, value: processCellValue(cell), type: cell.t || 'unknown', row_index: R, column: options.column });
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ else {
223
+ for (let R = sd.dims.startRow; R <= maxScanRow; ++R) {
224
+ rowsScanned++;
225
+ for (let C = sd.dims.startCol; C <= sd.dims.endCol; ++C) {
226
+ const address = XLSX.utils.encode_cell({ r: R, c: C });
227
+ const cell = sd.worksheet[address];
228
+ if (cell && cell.v !== undefined) {
229
+ const cellValue = String(processCellValue(cell)).toLowerCase();
230
+ if (cellValue.includes(searchTerm.toLowerCase())) {
231
+ totalMatchesFound++;
232
+ if (results.length < options.maxResults) {
233
+ results.push({ address, value: processCellValue(cell), type: cell.t || 'unknown', row_index: R });
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return { results, totalMatchesFound, rowsScanned };
241
+ }
242
+ filterRows(sheetIndex, column, operator, value, options) {
243
+ const sd = this.getSheetData(sheetIndex);
244
+ const filterColIdx = sd.headers.indexOf(column);
245
+ if (filterColIdx === -1) {
246
+ throw new Error(`Column "${column}" not found. Available columns: ${sd.headers.slice(0, 20).join(', ')}${sd.headers.length > 20 ? ` ... (${sd.headers.length} total)` : ''}`);
247
+ }
248
+ const filteredRows = [];
249
+ let totalMatchesFound = 0;
250
+ const maxScanRow = Math.min(sd.dims.startRow + options.maxRowsScan, sd.dims.endRow);
251
+ for (let R = sd.dims.startRow + 1; R <= maxScanRow; R++) {
252
+ const cellValue = getCellValue(sd.worksheet, R, sd.dims.startCol + filterColIdx);
253
+ let normalizedCell = cellValue;
254
+ let normalizedComp = value;
255
+ if (typeof normalizedCell === 'string' && normalizedCell.trim() !== '' && !isNaN(Number(normalizedCell))) {
256
+ normalizedCell = Number(normalizedCell);
257
+ }
258
+ if (typeof normalizedComp === 'string' && normalizedComp.trim() !== '' && !isNaN(Number(normalizedComp))) {
259
+ normalizedComp = Number(normalizedComp);
260
+ }
261
+ let matches = false;
262
+ switch (operator) {
263
+ case '=':
264
+ matches = normalizedCell === normalizedComp;
265
+ break;
266
+ case '!=':
267
+ matches = normalizedCell !== normalizedComp;
268
+ break;
269
+ case '>':
270
+ matches = normalizedCell > normalizedComp;
271
+ break;
272
+ case '<':
273
+ matches = normalizedCell < normalizedComp;
274
+ break;
275
+ case '>=':
276
+ matches = normalizedCell >= normalizedComp;
277
+ break;
278
+ case '<=':
279
+ matches = normalizedCell <= normalizedComp;
280
+ break;
281
+ case 'contains':
282
+ matches = String(normalizedCell).toLowerCase().includes(String(normalizedComp).toLowerCase());
283
+ break;
284
+ case 'starts_with':
285
+ matches = String(normalizedCell).toLowerCase().startsWith(String(normalizedComp).toLowerCase());
286
+ break;
287
+ case 'ends_with':
288
+ matches = String(normalizedCell).toLowerCase().endsWith(String(normalizedComp).toLowerCase());
289
+ break;
290
+ }
291
+ if (matches) {
292
+ totalMatchesFound++;
293
+ if (filteredRows.length < options.maxResults) {
294
+ filteredRows.push(rowToObject(sd.worksheet, R, sd.headers, sd.dims.startCol, sd.dims.endCol));
295
+ }
296
+ }
297
+ }
298
+ return {
299
+ matchedRows: filteredRows,
300
+ totalMatchesFound,
301
+ totalDataRows: sd.dims.endRow - sd.dims.startRow,
302
+ };
303
+ }
304
+ summarize(sheetIndex, columns, options) {
305
+ const sd = this.getSheetData(sheetIndex);
306
+ const columnsToAnalyze = columns && columns.length > 0 ? columns : sd.headers;
307
+ const colStats = new Map();
308
+ for (const col of columnsToAnalyze) {
309
+ colStats.set(col, { n: 0, mean: 0, m2: 0, min: Infinity, max: -Infinity, sum: 0 });
310
+ }
311
+ const maxScanRow = Math.min(sd.dims.startRow + options.maxRowsScan, sd.dims.endRow);
312
+ for (let R = sd.dims.startRow + 1; R <= maxScanRow; R++) {
313
+ for (let ci = 0; ci < columnsToAnalyze.length; ci++) {
314
+ const colName = columnsToAnalyze[ci];
315
+ const colIdx = sd.headers.indexOf(colName);
316
+ if (colIdx === -1)
317
+ continue;
318
+ const val = getCellValue(sd.worksheet, R, sd.dims.startCol + colIdx);
319
+ if (typeof val !== 'number' || isNaN(val))
320
+ continue;
321
+ const s = colStats.get(colName);
322
+ s.n++;
323
+ s.sum += val;
324
+ if (val < s.min)
325
+ s.min = val;
326
+ if (val > s.max)
327
+ s.max = val;
328
+ const delta = val - s.mean;
329
+ s.mean += delta / s.n;
330
+ const delta2 = val - s.mean;
331
+ s.m2 += delta * delta2;
332
+ }
333
+ }
334
+ const summaries = {};
335
+ for (const col of columnsToAnalyze) {
336
+ const s = colStats.get(col);
337
+ if (s.n > 0) {
338
+ summaries[col] = { sum: s.sum, avg: s.mean, min: s.min, max: s.max, std_dev: Math.sqrt(s.m2 / s.n) };
339
+ }
340
+ }
341
+ const processedRows = maxScanRow - sd.dims.startRow;
342
+ return { summaries, rowCount: processedRows };
343
+ }
344
+ groupBy(sheetIndex, groupColumn, aggColumn, aggType, options) {
345
+ const sd = this.getSheetData(sheetIndex);
346
+ const groupColIdx = sd.headers.indexOf(groupColumn);
347
+ const aggColIdx = sd.headers.indexOf(aggColumn);
348
+ if (groupColIdx === -1)
349
+ throw new Error(`Group column "${groupColumn}" not found`);
350
+ if (aggColIdx === -1)
351
+ throw new Error(`Aggregation column "${aggColumn}" not found`);
352
+ const maxScanRow = Math.min(sd.dims.startRow + options.maxRowsScan, sd.dims.endRow);
353
+ const grouped = {};
354
+ for (let R = sd.dims.startRow + 1; R <= maxScanRow; R++) {
355
+ const groupKey = String(getCellValue(sd.worksheet, R, sd.dims.startCol + groupColIdx) || '');
356
+ const aggValue = getCellValue(sd.worksheet, R, sd.dims.startCol + aggColIdx);
357
+ if (!grouped[groupKey])
358
+ grouped[groupKey] = [];
359
+ if (aggType === 'count') {
360
+ grouped[groupKey].push(1);
361
+ }
362
+ else if (typeof aggValue === 'number' && !isNaN(aggValue)) {
363
+ grouped[groupKey].push(aggValue);
364
+ }
365
+ }
366
+ const result = {};
367
+ for (const groupKey of Object.keys(grouped)) {
368
+ const values = grouped[groupKey];
369
+ if (values.length === 0) {
370
+ result[groupKey] = 0;
371
+ continue;
372
+ }
373
+ switch (aggType) {
374
+ case 'sum':
375
+ result[groupKey] = values.reduce((a, b) => a + b, 0);
376
+ break;
377
+ case 'count':
378
+ result[groupKey] = values.length;
379
+ break;
380
+ case 'avg':
381
+ result[groupKey] = values.reduce((a, b) => a + b, 0) / values.length;
382
+ break;
383
+ case 'min': {
384
+ const m = iterativeMin(values);
385
+ result[groupKey] = m !== undefined ? m : 0;
386
+ break;
387
+ }
388
+ case 'max': {
389
+ const m = iterativeMax(values);
390
+ result[groupKey] = m !== undefined ? m : 0;
391
+ break;
392
+ }
393
+ }
394
+ }
395
+ return { groups: result };
396
+ }
397
+ pivotSummary(sheetIndex, rowField, colField, valueField, agg, options) {
398
+ const sd = this.getSheetData(sheetIndex);
399
+ const rowFieldIdx = sd.headers.indexOf(rowField);
400
+ const colFieldIdx = sd.headers.indexOf(colField);
401
+ const valueFieldIdx = sd.headers.indexOf(valueField);
402
+ if (rowFieldIdx === -1)
403
+ throw new Error(`Row field "${rowField}" not found`);
404
+ if (colFieldIdx === -1)
405
+ throw new Error(`Column field "${colField}" not found`);
406
+ if (valueFieldIdx === -1)
407
+ throw new Error(`Value field "${valueField}" not found`);
408
+ const maxScanRow = Math.min(sd.dims.startRow + options.maxRowsScan, sd.dims.endRow);
409
+ const pivot = {};
410
+ const counts = {};
411
+ const allCols = new Set();
412
+ for (let R = sd.dims.startRow + 1; R <= maxScanRow; R++) {
413
+ const rowKey = String(getCellValue(sd.worksheet, R, sd.dims.startCol + rowFieldIdx) || '');
414
+ const colKey = String(getCellValue(sd.worksheet, R, sd.dims.startCol + colFieldIdx) || '');
415
+ const value = getCellValue(sd.worksheet, R, sd.dims.startCol + valueFieldIdx);
416
+ allCols.add(colKey);
417
+ const isNumeric = typeof value === 'number' && !isNaN(value);
418
+ if (!isNumeric)
419
+ continue;
420
+ if (!pivot[rowKey])
421
+ pivot[rowKey] = {};
422
+ if (!counts[rowKey])
423
+ counts[rowKey] = {};
424
+ if (!pivot[rowKey][colKey]) {
425
+ if (agg === 'min')
426
+ pivot[rowKey][colKey] = Infinity;
427
+ else if (agg === 'max')
428
+ pivot[rowKey][colKey] = -Infinity;
429
+ else
430
+ pivot[rowKey][colKey] = 0;
431
+ }
432
+ if (!counts[rowKey][colKey])
433
+ counts[rowKey][colKey] = 0;
434
+ counts[rowKey][colKey]++;
435
+ switch (agg) {
436
+ case 'sum':
437
+ case 'avg':
438
+ pivot[rowKey][colKey] += value;
439
+ break;
440
+ case 'min':
441
+ pivot[rowKey][colKey] = Math.min(pivot[rowKey][colKey], value);
442
+ break;
443
+ case 'max':
444
+ pivot[rowKey][colKey] = Math.max(pivot[rowKey][colKey], value);
445
+ break;
446
+ case 'count':
447
+ pivot[rowKey][colKey]++;
448
+ break;
449
+ }
450
+ }
451
+ if (agg === 'avg') {
452
+ for (const rowKey of Object.keys(pivot)) {
453
+ for (const colKey of Object.keys(pivot[rowKey])) {
454
+ const count = counts[rowKey][colKey];
455
+ if (count > 0)
456
+ pivot[rowKey][colKey] /= count;
457
+ }
458
+ }
459
+ }
460
+ return { columns: Array.from(allCols).sort(), pivot };
461
+ }
462
+ appendRows(sheetIndex, rows) {
463
+ const sheetName = this.workbook.SheetNames[sheetIndex];
464
+ const worksheet = this.workbook.Sheets[sheetName];
465
+ if (rows.length > 0 && Array.isArray(rows[0])) {
466
+ XLSX.utils.sheet_add_aoa(worksheet, rows, { origin: -1 });
467
+ }
468
+ else {
469
+ const hasExistingData = !!worksheet['!ref'];
470
+ const header = hasExistingData ? undefined : Object.keys(rows[0] || {});
471
+ XLSX.utils.sheet_add_json(worksheet, rows, { origin: -1, header });
472
+ }
473
+ writeAtomic(this.workbook, this.resolvedPath);
474
+ this.sheetMap.delete(sheetName);
475
+ }
476
+ updateCell(sheetIndex, cellAddress, value) {
477
+ if (!CELL_ADDRESS_REGEX.test(cellAddress)) {
478
+ throw new Error(`Invalid cell address "${cellAddress}". Expected format like "A1", "B5", "AA123".`);
479
+ }
480
+ const sheetName = this.workbook.SheetNames[sheetIndex];
481
+ const worksheet = this.workbook.Sheets[sheetName];
482
+ if (value === null) {
483
+ delete worksheet[cellAddress];
484
+ }
485
+ else {
486
+ let cellType;
487
+ if (typeof value === 'number')
488
+ cellType = 'n';
489
+ else if (typeof value === 'boolean')
490
+ cellType = 'b';
491
+ else
492
+ cellType = 's';
493
+ worksheet[cellAddress] = { v: value, t: cellType };
494
+ }
495
+ const newCellAddr = XLSX.utils.decode_cell(cellAddress);
496
+ const currentRef = worksheet['!ref'];
497
+ if (currentRef) {
498
+ const currentRange = XLSX.utils.decode_range(currentRef);
499
+ if (newCellAddr.r > currentRange.e.r || newCellAddr.c > currentRange.e.c) {
500
+ const maxReasonableRow = Math.max(currentRange.e.r + 1000, newCellAddr.r);
501
+ const maxReasonableCol = Math.max(currentRange.e.c + 26, newCellAddr.c);
502
+ const updatedRange = {
503
+ s: { r: Math.min(currentRange.s.r, newCellAddr.r), c: Math.min(currentRange.s.c, newCellAddr.c) },
504
+ e: { r: Math.min(Math.max(currentRange.e.r, newCellAddr.r), maxReasonableRow), c: Math.min(Math.max(currentRange.e.c, newCellAddr.c), maxReasonableCol) },
505
+ };
506
+ worksheet['!ref'] = XLSX.utils.encode_range(updatedRange);
507
+ }
508
+ }
509
+ else {
510
+ worksheet['!ref'] = XLSX.utils.encode_range({ s: newCellAddr, e: newCellAddr });
511
+ }
512
+ writeAtomic(this.workbook, this.resolvedPath);
513
+ this.sheetMap.delete(sheetName);
514
+ }
515
+ createFile(sheetName, data) {
516
+ if (fs.existsSync(this.resolvedPath)) {
517
+ invalidateCache(this.resolvedPath);
518
+ }
519
+ const workbook = XLSX.utils.book_new();
520
+ let worksheet;
521
+ let parsedData = data;
522
+ if (typeof parsedData === 'string') {
523
+ try {
524
+ parsedData = JSON.parse(parsedData);
525
+ }
526
+ catch (e) {
527
+ throw new Error(`Failed to parse data string as JSON: ${e}`);
528
+ }
529
+ }
530
+ if (!Array.isArray(parsedData))
531
+ throw new Error(`Data must be an array, received: ${typeof parsedData}`);
532
+ if (parsedData.length === 0) {
533
+ worksheet = XLSX.utils.aoa_to_sheet([]);
534
+ }
535
+ else if (Array.isArray(parsedData[0])) {
536
+ worksheet = XLSX.utils.aoa_to_sheet(parsedData);
537
+ }
538
+ else {
539
+ worksheet = XLSX.utils.json_to_sheet(parsedData);
540
+ }
541
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
542
+ writeAtomic(workbook, this.resolvedPath);
543
+ }
544
+ dispose() {
545
+ this.sheetMap.clear();
546
+ }
547
+ }