@wonderwhy-er/desktop-commander 0.2.23 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -55
- package/dist/custom-stdio.d.ts +1 -0
- package/dist/custom-stdio.js +19 -0
- package/dist/handlers/filesystem-handlers.d.ts +4 -0
- package/dist/handlers/filesystem-handlers.js +120 -14
- package/dist/handlers/node-handlers.d.ts +6 -0
- package/dist/handlers/node-handlers.js +73 -0
- package/dist/index.js +5 -3
- package/dist/search-manager.d.ts +25 -0
- package/dist/search-manager.js +212 -0
- package/dist/server.js +160 -73
- package/dist/terminal-manager.d.ts +56 -2
- package/dist/terminal-manager.js +169 -13
- package/dist/tools/edit.d.ts +28 -4
- package/dist/tools/edit.js +87 -4
- package/dist/tools/filesystem.d.ts +23 -12
- package/dist/tools/filesystem.js +201 -416
- package/dist/tools/improved-process-tools.d.ts +2 -2
- package/dist/tools/improved-process-tools.js +244 -214
- package/dist/tools/mime-types.d.ts +1 -0
- package/dist/tools/mime-types.js +7 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +96 -0
- package/dist/tools/pdf/markdown.d.ts +7 -0
- package/dist/tools/pdf/markdown.js +37 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/schemas.d.ts +167 -12
- package/dist/tools/schemas.js +54 -5
- package/dist/types.d.ts +2 -1
- package/dist/utils/feature-flags.js +7 -4
- package/dist/utils/files/base.d.ts +167 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +416 -0
- package/dist/utils/files/factory.d.ts +40 -0
- package/dist/utils/files/factory.js +101 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +357 -0
- package/dist/utils/ripgrep-resolver.js +3 -2
- package/dist/utils/system-info.d.ts +5 -0
- package/dist/utils/system-info.js +71 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -3
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel file handler using ExcelJS
|
|
3
|
+
* Handles reading, writing, and editing Excel files (.xlsx, .xls, .xlsm)
|
|
4
|
+
*/
|
|
5
|
+
import ExcelJS from 'exceljs';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
// File size limit: 10MB
|
|
8
|
+
const FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* Excel file handler implementation using ExcelJS
|
|
11
|
+
* Supports: .xlsx, .xls, .xlsm files
|
|
12
|
+
*/
|
|
13
|
+
export class ExcelFileHandler {
|
|
14
|
+
canHandle(path) {
|
|
15
|
+
const ext = path.toLowerCase();
|
|
16
|
+
return ext.endsWith('.xlsx') || ext.endsWith('.xls') || ext.endsWith('.xlsm');
|
|
17
|
+
}
|
|
18
|
+
async read(path, options) {
|
|
19
|
+
await this.checkFileSize(path);
|
|
20
|
+
const workbook = new ExcelJS.Workbook();
|
|
21
|
+
await workbook.xlsx.readFile(path);
|
|
22
|
+
const metadata = await this.extractMetadata(workbook, path);
|
|
23
|
+
const { sheetName, data, totalRows, returnedRows } = this.worksheetToArray(workbook, options?.sheet, options?.range, options?.offset, options?.length);
|
|
24
|
+
// Format output with sheet info header, usage hint, and JSON data
|
|
25
|
+
const paginationInfo = totalRows > returnedRows
|
|
26
|
+
? `\n[Showing rows ${(options?.offset || 0) + 1}-${(options?.offset || 0) + returnedRows} of ${totalRows} total. Use offset/length to paginate.]`
|
|
27
|
+
: '';
|
|
28
|
+
const content = `[Sheet: '${sheetName}' from ${path}]${paginationInfo}
|
|
29
|
+
[To MODIFY cells: use edit_block with range param, e.g., edit_block(path, {range: "Sheet1!E5", content: [[newValue]]})]
|
|
30
|
+
|
|
31
|
+
${JSON.stringify(data)}`;
|
|
32
|
+
return {
|
|
33
|
+
content,
|
|
34
|
+
mimeType: 'application/json',
|
|
35
|
+
metadata: {
|
|
36
|
+
isExcelFile: true,
|
|
37
|
+
sheets: metadata.sheets,
|
|
38
|
+
fileSize: metadata.fileSize,
|
|
39
|
+
isLargeFile: metadata.isLargeFile
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async write(path, content, mode) {
|
|
44
|
+
// Check existing file size if it exists
|
|
45
|
+
try {
|
|
46
|
+
await this.checkFileSize(path);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// File doesn't exist - that's fine for write
|
|
50
|
+
if (error.code !== 'ENOENT' &&
|
|
51
|
+
!(error instanceof Error && error.message.includes('ENOENT'))) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Parse content
|
|
56
|
+
let parsedContent = content;
|
|
57
|
+
if (typeof content === 'string') {
|
|
58
|
+
try {
|
|
59
|
+
parsedContent = JSON.parse(content);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error('Invalid content format. Expected JSON string with 2D array or object with sheet names.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Handle append mode by finding last row and writing after it
|
|
66
|
+
if (mode === 'append') {
|
|
67
|
+
try {
|
|
68
|
+
const workbook = new ExcelJS.Workbook();
|
|
69
|
+
await workbook.xlsx.readFile(path);
|
|
70
|
+
if (Array.isArray(parsedContent)) {
|
|
71
|
+
// Append to Sheet1
|
|
72
|
+
let worksheet = workbook.getWorksheet('Sheet1');
|
|
73
|
+
if (!worksheet) {
|
|
74
|
+
worksheet = workbook.addWorksheet('Sheet1');
|
|
75
|
+
}
|
|
76
|
+
const startRow = (worksheet.actualRowCount || 0) + 1;
|
|
77
|
+
this.writeRowsStartingAt(worksheet, startRow, parsedContent);
|
|
78
|
+
}
|
|
79
|
+
else if (typeof parsedContent === 'object' && parsedContent !== null) {
|
|
80
|
+
// Append to each named sheet
|
|
81
|
+
for (const [sheetName, data] of Object.entries(parsedContent)) {
|
|
82
|
+
if (Array.isArray(data)) {
|
|
83
|
+
let worksheet = workbook.getWorksheet(sheetName);
|
|
84
|
+
if (!worksheet) {
|
|
85
|
+
worksheet = workbook.addWorksheet(sheetName);
|
|
86
|
+
}
|
|
87
|
+
const startRow = (worksheet.actualRowCount || 0) + 1;
|
|
88
|
+
this.writeRowsStartingAt(worksheet, startRow, data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await workbook.xlsx.writeFile(path);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// File doesn't exist - fall through to create new file
|
|
97
|
+
if (error.code !== 'ENOENT' &&
|
|
98
|
+
!(error instanceof Error && error.message.includes('ENOENT'))) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Rewrite mode (or append to non-existent file): create new workbook
|
|
104
|
+
const workbook = new ExcelJS.Workbook();
|
|
105
|
+
if (Array.isArray(parsedContent)) {
|
|
106
|
+
// Single sheet from 2D array
|
|
107
|
+
this.writeDataToSheet(workbook, 'Sheet1', parsedContent);
|
|
108
|
+
}
|
|
109
|
+
else if (typeof parsedContent === 'object' && parsedContent !== null) {
|
|
110
|
+
// Object with sheet names as keys
|
|
111
|
+
for (const [sheetName, data] of Object.entries(parsedContent)) {
|
|
112
|
+
if (Array.isArray(data)) {
|
|
113
|
+
this.writeDataToSheet(workbook, sheetName, data);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error('Invalid content format. Expected 2D array or object with sheet names.');
|
|
119
|
+
}
|
|
120
|
+
await workbook.xlsx.writeFile(path);
|
|
121
|
+
}
|
|
122
|
+
async editRange(path, range, content, options) {
|
|
123
|
+
// Verify file exists and check size
|
|
124
|
+
try {
|
|
125
|
+
await this.checkFileSize(path);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT' ||
|
|
129
|
+
(error instanceof Error && error.message.includes('ENOENT'))) {
|
|
130
|
+
throw new Error(`File not found: ${path}`);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
// Validate content
|
|
135
|
+
if (!Array.isArray(content)) {
|
|
136
|
+
throw new Error('Content must be a 2D array for range editing');
|
|
137
|
+
}
|
|
138
|
+
// Parse range: "Sheet1!A1:C10" or "Sheet1"
|
|
139
|
+
const [sheetName, cellRange] = this.parseRange(range);
|
|
140
|
+
const workbook = new ExcelJS.Workbook();
|
|
141
|
+
await workbook.xlsx.readFile(path);
|
|
142
|
+
// Get or create sheet
|
|
143
|
+
let worksheet = workbook.getWorksheet(sheetName);
|
|
144
|
+
if (!worksheet) {
|
|
145
|
+
worksheet = workbook.addWorksheet(sheetName);
|
|
146
|
+
}
|
|
147
|
+
if (cellRange) {
|
|
148
|
+
// Write to specific range
|
|
149
|
+
const { startRow, startCol } = this.parseCellRange(cellRange);
|
|
150
|
+
for (let r = 0; r < content.length; r++) {
|
|
151
|
+
const rowData = content[r];
|
|
152
|
+
if (!Array.isArray(rowData))
|
|
153
|
+
continue;
|
|
154
|
+
for (let c = 0; c < rowData.length; c++) {
|
|
155
|
+
const cell = worksheet.getCell(startRow + r, startCol + c);
|
|
156
|
+
const value = rowData[c];
|
|
157
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
158
|
+
cell.value = { formula: value.substring(1) };
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
cell.value = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Replace entire sheet content
|
|
168
|
+
// Clear existing data
|
|
169
|
+
worksheet.eachRow((row, rowNumber) => {
|
|
170
|
+
row.eachCell((cell) => {
|
|
171
|
+
cell.value = null;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
// Write new data
|
|
175
|
+
for (let r = 0; r < content.length; r++) {
|
|
176
|
+
const rowData = content[r];
|
|
177
|
+
if (!Array.isArray(rowData))
|
|
178
|
+
continue;
|
|
179
|
+
const row = worksheet.getRow(r + 1);
|
|
180
|
+
for (let c = 0; c < rowData.length; c++) {
|
|
181
|
+
const value = rowData[c];
|
|
182
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
183
|
+
row.getCell(c + 1).value = { formula: value.substring(1) };
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
row.getCell(c + 1).value = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
row.commit();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
await workbook.xlsx.writeFile(path);
|
|
193
|
+
return { success: true, editsApplied: 1 };
|
|
194
|
+
}
|
|
195
|
+
async getInfo(path) {
|
|
196
|
+
const stats = await fs.stat(path);
|
|
197
|
+
try {
|
|
198
|
+
const workbook = new ExcelJS.Workbook();
|
|
199
|
+
await workbook.xlsx.readFile(path);
|
|
200
|
+
const metadata = await this.extractMetadata(workbook, path);
|
|
201
|
+
return {
|
|
202
|
+
size: stats.size,
|
|
203
|
+
created: stats.birthtime,
|
|
204
|
+
modified: stats.mtime,
|
|
205
|
+
accessed: stats.atime,
|
|
206
|
+
isDirectory: stats.isDirectory(),
|
|
207
|
+
isFile: stats.isFile(),
|
|
208
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
209
|
+
fileType: 'excel',
|
|
210
|
+
metadata: {
|
|
211
|
+
isExcelFile: true,
|
|
212
|
+
sheets: metadata.sheets,
|
|
213
|
+
fileSize: metadata.fileSize,
|
|
214
|
+
isLargeFile: metadata.isLargeFile
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
size: stats.size,
|
|
221
|
+
created: stats.birthtime,
|
|
222
|
+
modified: stats.mtime,
|
|
223
|
+
accessed: stats.atime,
|
|
224
|
+
isDirectory: stats.isDirectory(),
|
|
225
|
+
isFile: stats.isFile(),
|
|
226
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
227
|
+
fileType: 'excel',
|
|
228
|
+
metadata: {
|
|
229
|
+
isExcelFile: true,
|
|
230
|
+
fileSize: stats.size,
|
|
231
|
+
error: true,
|
|
232
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ========== Private Helpers ==========
|
|
238
|
+
async checkFileSize(path) {
|
|
239
|
+
const stats = await fs.stat(path);
|
|
240
|
+
if (stats.size > FILE_SIZE_LIMIT) {
|
|
241
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
242
|
+
throw new Error(`Excel file size (${sizeMB}MB) exceeds 10MB limit. ` +
|
|
243
|
+
`Consider using specialized tools for large file processing.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async extractMetadata(workbook, path) {
|
|
247
|
+
const stats = await fs.stat(path);
|
|
248
|
+
const sheets = workbook.worksheets.map(ws => ({
|
|
249
|
+
name: ws.name,
|
|
250
|
+
rowCount: ws.actualRowCount || 0,
|
|
251
|
+
colCount: ws.actualColumnCount || 0
|
|
252
|
+
}));
|
|
253
|
+
return {
|
|
254
|
+
sheets,
|
|
255
|
+
fileSize: stats.size,
|
|
256
|
+
isLargeFile: stats.size > FILE_SIZE_LIMIT
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
worksheetToArray(workbook, sheetRef, range, offset, length) {
|
|
260
|
+
if (workbook.worksheets.length === 0) {
|
|
261
|
+
return { sheetName: '', data: [], totalRows: 0, returnedRows: 0 };
|
|
262
|
+
}
|
|
263
|
+
// Find target worksheet
|
|
264
|
+
let worksheet;
|
|
265
|
+
let sheetName;
|
|
266
|
+
if (sheetRef === undefined) {
|
|
267
|
+
worksheet = workbook.worksheets[0];
|
|
268
|
+
sheetName = worksheet.name;
|
|
269
|
+
}
|
|
270
|
+
else if (typeof sheetRef === 'number') {
|
|
271
|
+
if (sheetRef < 0 || sheetRef >= workbook.worksheets.length) {
|
|
272
|
+
throw new Error(`Sheet index ${sheetRef} out of range (0-${workbook.worksheets.length - 1})`);
|
|
273
|
+
}
|
|
274
|
+
worksheet = workbook.worksheets[sheetRef];
|
|
275
|
+
sheetName = worksheet.name;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
worksheet = workbook.getWorksheet(sheetRef);
|
|
279
|
+
if (!worksheet) {
|
|
280
|
+
const available = workbook.worksheets.map(ws => ws.name).join(', ');
|
|
281
|
+
throw new Error(`Sheet "${sheetRef}" not found. Available sheets: ${available}`);
|
|
282
|
+
}
|
|
283
|
+
sheetName = sheetRef;
|
|
284
|
+
}
|
|
285
|
+
// Determine range to read
|
|
286
|
+
let startRow = 1;
|
|
287
|
+
let endRow = worksheet.actualRowCount || 1;
|
|
288
|
+
let startCol = 1;
|
|
289
|
+
let endCol = worksheet.actualColumnCount || 1;
|
|
290
|
+
if (range) {
|
|
291
|
+
const parsed = this.parseCellRange(range);
|
|
292
|
+
startRow = parsed.startRow;
|
|
293
|
+
startCol = parsed.startCol;
|
|
294
|
+
if (parsed.endRow)
|
|
295
|
+
endRow = parsed.endRow;
|
|
296
|
+
if (parsed.endCol)
|
|
297
|
+
endCol = parsed.endCol;
|
|
298
|
+
}
|
|
299
|
+
// Calculate total rows before pagination
|
|
300
|
+
const totalRows = endRow - startRow + 1;
|
|
301
|
+
// Apply offset/length pagination (row-based, matching text file behavior)
|
|
302
|
+
if (offset !== undefined) {
|
|
303
|
+
if (offset < 0) {
|
|
304
|
+
// Negative offset: last N rows (like text files)
|
|
305
|
+
// offset: -10 means "last 10 rows"
|
|
306
|
+
const lastNRows = Math.abs(offset);
|
|
307
|
+
startRow = Math.max(startRow, endRow - lastNRows + 1);
|
|
308
|
+
}
|
|
309
|
+
else if (offset > 0) {
|
|
310
|
+
// Positive offset: skip first N rows
|
|
311
|
+
startRow = startRow + offset;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Apply length limit (only for positive offset or no offset)
|
|
315
|
+
if (length !== undefined && length > 0 && (offset === undefined || offset >= 0)) {
|
|
316
|
+
endRow = Math.min(endRow, startRow + length - 1);
|
|
317
|
+
}
|
|
318
|
+
// Ensure valid range
|
|
319
|
+
if (startRow > endRow) {
|
|
320
|
+
return { sheetName, data: [], totalRows, returnedRows: 0 };
|
|
321
|
+
}
|
|
322
|
+
// Build 2D array (preserving types)
|
|
323
|
+
const data = [];
|
|
324
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
325
|
+
const row = worksheet.getRow(r);
|
|
326
|
+
const rowData = [];
|
|
327
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
328
|
+
const cell = row.getCell(c);
|
|
329
|
+
let value = null;
|
|
330
|
+
if (cell.value !== null && cell.value !== undefined) {
|
|
331
|
+
if (typeof cell.value === 'object') {
|
|
332
|
+
// Handle formula results, rich text, etc.
|
|
333
|
+
if ('result' in cell.value) {
|
|
334
|
+
value = cell.value.result ?? null;
|
|
335
|
+
}
|
|
336
|
+
else if ('richText' in cell.value) {
|
|
337
|
+
value = cell.value.richText.map((rt) => rt.text).join('');
|
|
338
|
+
}
|
|
339
|
+
else if ('text' in cell.value) {
|
|
340
|
+
value = cell.value.text;
|
|
341
|
+
}
|
|
342
|
+
else if (cell.value instanceof Date) {
|
|
343
|
+
value = cell.value.toISOString();
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
value = String(cell.value);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// Preserve native types (string, number, boolean)
|
|
351
|
+
value = cell.value;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
rowData.push(value);
|
|
355
|
+
}
|
|
356
|
+
data.push(rowData);
|
|
357
|
+
}
|
|
358
|
+
return { sheetName, data, totalRows, returnedRows: data.length };
|
|
359
|
+
}
|
|
360
|
+
writeDataToSheet(workbook, sheetName, data) {
|
|
361
|
+
// Remove existing sheet if it exists
|
|
362
|
+
const existing = workbook.getWorksheet(sheetName);
|
|
363
|
+
if (existing) {
|
|
364
|
+
workbook.removeWorksheet(existing.id);
|
|
365
|
+
}
|
|
366
|
+
const worksheet = workbook.addWorksheet(sheetName);
|
|
367
|
+
this.writeRowsStartingAt(worksheet, 1, data);
|
|
368
|
+
}
|
|
369
|
+
writeRowsStartingAt(worksheet, startRow, data) {
|
|
370
|
+
for (let r = 0; r < data.length; r++) {
|
|
371
|
+
const rowData = data[r];
|
|
372
|
+
if (!Array.isArray(rowData))
|
|
373
|
+
continue;
|
|
374
|
+
const row = worksheet.getRow(startRow + r);
|
|
375
|
+
for (let c = 0; c < rowData.length; c++) {
|
|
376
|
+
const value = rowData[c];
|
|
377
|
+
if (typeof value === 'string' && value.startsWith('=')) {
|
|
378
|
+
row.getCell(c + 1).value = { formula: value.substring(1) };
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
row.getCell(c + 1).value = value;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
row.commit();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
parseRange(range) {
|
|
388
|
+
if (range.includes('!')) {
|
|
389
|
+
const [sheetName, cellRange] = range.split('!');
|
|
390
|
+
return [sheetName, cellRange];
|
|
391
|
+
}
|
|
392
|
+
return [range, null];
|
|
393
|
+
}
|
|
394
|
+
parseCellRange(range) {
|
|
395
|
+
// Parse A1 or A1:C10 format
|
|
396
|
+
const match = range.match(/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i);
|
|
397
|
+
if (!match) {
|
|
398
|
+
throw new Error(`Invalid cell range: ${range}`);
|
|
399
|
+
}
|
|
400
|
+
const startCol = this.columnToNumber(match[1]);
|
|
401
|
+
const startRow = parseInt(match[2], 10);
|
|
402
|
+
if (match[3] && match[4]) {
|
|
403
|
+
const endCol = this.columnToNumber(match[3]);
|
|
404
|
+
const endRow = parseInt(match[4], 10);
|
|
405
|
+
return { startRow, startCol, endRow, endCol };
|
|
406
|
+
}
|
|
407
|
+
return { startRow, startCol };
|
|
408
|
+
}
|
|
409
|
+
columnToNumber(col) {
|
|
410
|
+
let result = 0;
|
|
411
|
+
for (let i = 0; i < col.length; i++) {
|
|
412
|
+
result = result * 26 + col.charCodeAt(i) - 64;
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory pattern for creating appropriate file handlers
|
|
3
|
+
* Routes file operations to the correct handler based on file type
|
|
4
|
+
*
|
|
5
|
+
* Each handler implements canHandle() which can be sync (extension-based)
|
|
6
|
+
* or async (content-based like BinaryFileHandler using isBinaryFile)
|
|
7
|
+
*/
|
|
8
|
+
import { FileHandler } from './base.js';
|
|
9
|
+
/**
|
|
10
|
+
* Get the appropriate file handler for a given file path
|
|
11
|
+
*
|
|
12
|
+
* Each handler's canHandle() determines if it can process the file.
|
|
13
|
+
* Extension-based handlers (Excel, Image) return sync boolean.
|
|
14
|
+
* BinaryFileHandler uses async isBinaryFile for content-based detection.
|
|
15
|
+
*
|
|
16
|
+
* Priority order:
|
|
17
|
+
* 1. PDF files (extension based)
|
|
18
|
+
* 2. Excel files (xlsx, xls, xlsm) - extension based
|
|
19
|
+
* 3. Image files (png, jpg, gif, webp) - extension based
|
|
20
|
+
* 4. Binary files - content-based detection via isBinaryFile
|
|
21
|
+
* 5. Text files (default)
|
|
22
|
+
*
|
|
23
|
+
* @param filePath File path to get handler for
|
|
24
|
+
* @returns FileHandler instance that can handle this file
|
|
25
|
+
*/
|
|
26
|
+
export declare function getFileHandler(filePath: string): Promise<FileHandler>;
|
|
27
|
+
/**
|
|
28
|
+
* Check if a file path is an Excel file
|
|
29
|
+
* Delegates to ExcelFileHandler.canHandle to avoid duplicating extension logic
|
|
30
|
+
* @param path File path
|
|
31
|
+
* @returns true if file is Excel format
|
|
32
|
+
*/
|
|
33
|
+
export declare function isExcelFile(path: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a file path is an image file
|
|
36
|
+
* Delegates to ImageFileHandler.canHandle to avoid duplicating extension logic
|
|
37
|
+
* @param path File path
|
|
38
|
+
* @returns true if file is an image format
|
|
39
|
+
*/
|
|
40
|
+
export declare function isImageFile(path: string): boolean;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory pattern for creating appropriate file handlers
|
|
3
|
+
* Routes file operations to the correct handler based on file type
|
|
4
|
+
*
|
|
5
|
+
* Each handler implements canHandle() which can be sync (extension-based)
|
|
6
|
+
* or async (content-based like BinaryFileHandler using isBinaryFile)
|
|
7
|
+
*/
|
|
8
|
+
import { TextFileHandler } from './text.js';
|
|
9
|
+
import { ImageFileHandler } from './image.js';
|
|
10
|
+
import { BinaryFileHandler } from './binary.js';
|
|
11
|
+
import { ExcelFileHandler } from './excel.js';
|
|
12
|
+
import { PdfFileHandler } from './pdf.js';
|
|
13
|
+
// Singleton instances of each handler
|
|
14
|
+
let excelHandler = null;
|
|
15
|
+
let imageHandler = null;
|
|
16
|
+
let textHandler = null;
|
|
17
|
+
let binaryHandler = null;
|
|
18
|
+
let pdfHandler = null;
|
|
19
|
+
/**
|
|
20
|
+
* Initialize handlers (lazy initialization)
|
|
21
|
+
*/
|
|
22
|
+
function getExcelHandler() {
|
|
23
|
+
if (!excelHandler)
|
|
24
|
+
excelHandler = new ExcelFileHandler();
|
|
25
|
+
return excelHandler;
|
|
26
|
+
}
|
|
27
|
+
function getImageHandler() {
|
|
28
|
+
if (!imageHandler)
|
|
29
|
+
imageHandler = new ImageFileHandler();
|
|
30
|
+
return imageHandler;
|
|
31
|
+
}
|
|
32
|
+
function getTextHandler() {
|
|
33
|
+
if (!textHandler)
|
|
34
|
+
textHandler = new TextFileHandler();
|
|
35
|
+
return textHandler;
|
|
36
|
+
}
|
|
37
|
+
function getBinaryHandler() {
|
|
38
|
+
if (!binaryHandler)
|
|
39
|
+
binaryHandler = new BinaryFileHandler();
|
|
40
|
+
return binaryHandler;
|
|
41
|
+
}
|
|
42
|
+
function getPdfHandler() {
|
|
43
|
+
if (!pdfHandler)
|
|
44
|
+
pdfHandler = new PdfFileHandler();
|
|
45
|
+
return pdfHandler;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the appropriate file handler for a given file path
|
|
49
|
+
*
|
|
50
|
+
* Each handler's canHandle() determines if it can process the file.
|
|
51
|
+
* Extension-based handlers (Excel, Image) return sync boolean.
|
|
52
|
+
* BinaryFileHandler uses async isBinaryFile for content-based detection.
|
|
53
|
+
*
|
|
54
|
+
* Priority order:
|
|
55
|
+
* 1. PDF files (extension based)
|
|
56
|
+
* 2. Excel files (xlsx, xls, xlsm) - extension based
|
|
57
|
+
* 3. Image files (png, jpg, gif, webp) - extension based
|
|
58
|
+
* 4. Binary files - content-based detection via isBinaryFile
|
|
59
|
+
* 5. Text files (default)
|
|
60
|
+
*
|
|
61
|
+
* @param filePath File path to get handler for
|
|
62
|
+
* @returns FileHandler instance that can handle this file
|
|
63
|
+
*/
|
|
64
|
+
export async function getFileHandler(filePath) {
|
|
65
|
+
// Check PDF first (extension-based, sync)
|
|
66
|
+
if (getPdfHandler().canHandle(filePath)) {
|
|
67
|
+
return getPdfHandler();
|
|
68
|
+
}
|
|
69
|
+
// Check Excel (extension-based, sync)
|
|
70
|
+
if (getExcelHandler().canHandle(filePath)) {
|
|
71
|
+
return getExcelHandler();
|
|
72
|
+
}
|
|
73
|
+
// Check Image (extension-based, sync - images are binary but handled specially)
|
|
74
|
+
if (getImageHandler().canHandle(filePath)) {
|
|
75
|
+
return getImageHandler();
|
|
76
|
+
}
|
|
77
|
+
// Check Binary (content-based, async via isBinaryFile)
|
|
78
|
+
if (await getBinaryHandler().canHandle(filePath)) {
|
|
79
|
+
return getBinaryHandler();
|
|
80
|
+
}
|
|
81
|
+
// Default to text handler
|
|
82
|
+
return getTextHandler();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if a file path is an Excel file
|
|
86
|
+
* Delegates to ExcelFileHandler.canHandle to avoid duplicating extension logic
|
|
87
|
+
* @param path File path
|
|
88
|
+
* @returns true if file is Excel format
|
|
89
|
+
*/
|
|
90
|
+
export function isExcelFile(path) {
|
|
91
|
+
return getExcelHandler().canHandle(path);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a file path is an image file
|
|
95
|
+
* Delegates to ImageFileHandler.canHandle to avoid duplicating extension logic
|
|
96
|
+
* @param path File path
|
|
97
|
+
* @returns true if file is an image format
|
|
98
|
+
*/
|
|
99
|
+
export function isImageFile(path) {
|
|
100
|
+
return getImageHandler().canHandle(path);
|
|
101
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image file handler
|
|
3
|
+
* Handles reading image files and converting to base64
|
|
4
|
+
*/
|
|
5
|
+
import { FileHandler, ReadOptions, FileResult, FileInfo } from './base.js';
|
|
6
|
+
/**
|
|
7
|
+
* Image file handler implementation
|
|
8
|
+
* Supports: PNG, JPEG, GIF, WebP, BMP, SVG
|
|
9
|
+
*/
|
|
10
|
+
export declare class ImageFileHandler implements FileHandler {
|
|
11
|
+
private static readonly IMAGE_EXTENSIONS;
|
|
12
|
+
private static readonly IMAGE_MIME_TYPES;
|
|
13
|
+
canHandle(path: string): boolean;
|
|
14
|
+
read(path: string, options?: ReadOptions): Promise<FileResult>;
|
|
15
|
+
write(path: string, content: Buffer | string): Promise<void>;
|
|
16
|
+
getInfo(path: string): Promise<FileInfo>;
|
|
17
|
+
/**
|
|
18
|
+
* Get MIME type for image based on file extension
|
|
19
|
+
*/
|
|
20
|
+
private getMimeType;
|
|
21
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image file handler
|
|
3
|
+
* Handles reading image files and converting to base64
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
/**
|
|
7
|
+
* Image file handler implementation
|
|
8
|
+
* Supports: PNG, JPEG, GIF, WebP, BMP, SVG
|
|
9
|
+
*/
|
|
10
|
+
export class ImageFileHandler {
|
|
11
|
+
canHandle(path) {
|
|
12
|
+
const lowerPath = path.toLowerCase();
|
|
13
|
+
return ImageFileHandler.IMAGE_EXTENSIONS.some(ext => lowerPath.endsWith(ext));
|
|
14
|
+
}
|
|
15
|
+
async read(path, options) {
|
|
16
|
+
// Images are always read in full, ignoring offset and length
|
|
17
|
+
const buffer = await fs.readFile(path);
|
|
18
|
+
const content = buffer.toString('base64');
|
|
19
|
+
const mimeType = this.getMimeType(path);
|
|
20
|
+
return {
|
|
21
|
+
content,
|
|
22
|
+
mimeType,
|
|
23
|
+
metadata: {
|
|
24
|
+
isImage: true
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async write(path, content) {
|
|
29
|
+
// If content is base64 string, convert to buffer
|
|
30
|
+
if (typeof content === 'string') {
|
|
31
|
+
const buffer = Buffer.from(content, 'base64');
|
|
32
|
+
await fs.writeFile(path, buffer);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await fs.writeFile(path, content);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async getInfo(path) {
|
|
39
|
+
const stats = await fs.stat(path);
|
|
40
|
+
return {
|
|
41
|
+
size: stats.size,
|
|
42
|
+
created: stats.birthtime,
|
|
43
|
+
modified: stats.mtime,
|
|
44
|
+
accessed: stats.atime,
|
|
45
|
+
isDirectory: stats.isDirectory(),
|
|
46
|
+
isFile: stats.isFile(),
|
|
47
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
48
|
+
fileType: 'image',
|
|
49
|
+
metadata: {
|
|
50
|
+
isImage: true
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get MIME type for image based on file extension
|
|
56
|
+
*/
|
|
57
|
+
getMimeType(path) {
|
|
58
|
+
const lowerPath = path.toLowerCase();
|
|
59
|
+
for (const [ext, mimeType] of Object.entries(ImageFileHandler.IMAGE_MIME_TYPES)) {
|
|
60
|
+
if (lowerPath.endsWith(ext)) {
|
|
61
|
+
return mimeType;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return 'application/octet-stream'; // Fallback
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
ImageFileHandler.IMAGE_EXTENSIONS = [
|
|
68
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'
|
|
69
|
+
];
|
|
70
|
+
ImageFileHandler.IMAGE_MIME_TYPES = {
|
|
71
|
+
'.png': 'image/png',
|
|
72
|
+
'.jpg': 'image/jpeg',
|
|
73
|
+
'.jpeg': 'image/jpeg',
|
|
74
|
+
'.gif': 'image/gif',
|
|
75
|
+
'.webp': 'image/webp',
|
|
76
|
+
'.bmp': 'image/bmp',
|
|
77
|
+
'.svg': 'image/svg+xml'
|
|
78
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File handling system
|
|
3
|
+
* Exports all file handlers, interfaces, and utilities
|
|
4
|
+
*/
|
|
5
|
+
export * from './base.js';
|
|
6
|
+
export { getFileHandler, isExcelFile, isImageFile } from './factory.js';
|
|
7
|
+
export { TextFileHandler } from './text.js';
|
|
8
|
+
export { ImageFileHandler } from './image.js';
|
|
9
|
+
export { BinaryFileHandler } from './binary.js';
|
|
10
|
+
export { ExcelFileHandler } from './excel.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File handling system
|
|
3
|
+
* Exports all file handlers, interfaces, and utilities
|
|
4
|
+
*/
|
|
5
|
+
// Base interfaces and types
|
|
6
|
+
export * from './base.js';
|
|
7
|
+
// Factory function
|
|
8
|
+
export { getFileHandler, isExcelFile, isImageFile } from './factory.js';
|
|
9
|
+
// File handlers
|
|
10
|
+
export { TextFileHandler } from './text.js';
|
|
11
|
+
export { ImageFileHandler } from './image.js';
|
|
12
|
+
export { BinaryFileHandler } from './binary.js';
|
|
13
|
+
export { ExcelFileHandler } from './excel.js';
|