xlsx-for-ai 1.1.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.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/cursor-rule-template/read-xlsx.mdc +50 -0
- package/index.js +606 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 senoff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# xlsx-for-ai
|
|
2
|
+
|
|
3
|
+
Converts `.xlsx` files into rich text or JSON dumps that AI coding agents can actually read.
|
|
4
|
+
|
|
5
|
+
AI tools — Claude, Cursor, Copilot, ChatGPT, and other LLM coding agents — can read text files but **not** `.xlsx` binaries. This CLI bridges the gap. It extracts everything a human would see in Excel and writes it to a plain text file (or structured JSON):
|
|
6
|
+
|
|
7
|
+
- **Values** — strings, numbers, dates
|
|
8
|
+
- **Formulas** — the actual formula expression, plus shared-formula references
|
|
9
|
+
- **Formatting** — bold, italic, font colors, background fills
|
|
10
|
+
- **Number formats** — percentages, currency, custom patterns
|
|
11
|
+
- **Layout** — column widths, frozen panes, merged cells, alignment
|
|
12
|
+
- **Hyperlinks** — URLs embedded in cells
|
|
13
|
+
- **Comments / notes** — cell annotations
|
|
14
|
+
- **Named ranges** — workbook-defined names and their references
|
|
15
|
+
- **Hidden rows & columns** — flagged so the AI knows data is suppressed
|
|
16
|
+
- **Data validation** — dropdown lists, numeric constraints
|
|
17
|
+
- **Tables** — Excel Table objects with their names and column headers
|
|
18
|
+
- **Images & charts** — existence and position noted (content not rendered)
|
|
19
|
+
- **Auto-filters** — active filter ranges
|
|
20
|
+
- **Print areas** — defined print regions
|
|
21
|
+
|
|
22
|
+
> Previously published as **`cursor-reads-xlsx`**. The old name still works as an alias on the CLI, but please install the new package: `npm install -g xlsx-for-ai`.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g xlsx-for-ai
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or run directly with npx (no install needed):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx xlsx-for-ai budget.xlsx
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Dump all sheets
|
|
40
|
+
npx xlsx-for-ai data.xlsx
|
|
41
|
+
|
|
42
|
+
# Dump a specific sheet
|
|
43
|
+
npx xlsx-for-ai data.xlsx "Sheet1"
|
|
44
|
+
|
|
45
|
+
# List sheet names and dimensions without dumping
|
|
46
|
+
npx xlsx-for-ai data.xlsx --list-sheets
|
|
47
|
+
|
|
48
|
+
# Print to stdout instead of writing files
|
|
49
|
+
npx xlsx-for-ai data.xlsx --stdout
|
|
50
|
+
|
|
51
|
+
# Limit to first 200 rows per sheet (useful for huge files)
|
|
52
|
+
npx xlsx-for-ai data.xlsx --max-rows 200
|
|
53
|
+
|
|
54
|
+
# Limit to first 8 columns (useful for very wide sheets)
|
|
55
|
+
npx xlsx-for-ai data.xlsx --max-cols 8
|
|
56
|
+
|
|
57
|
+
# Suppress noisy default tags (default text colors, white fills, etc.)
|
|
58
|
+
npx xlsx-for-ai data.xlsx --stdout --compact
|
|
59
|
+
|
|
60
|
+
# Emit structured JSON (one entry per cell) instead of the text dump
|
|
61
|
+
npx xlsx-for-ai data.xlsx --json --stdout > out.json
|
|
62
|
+
|
|
63
|
+
# Combine flags
|
|
64
|
+
npx xlsx-for-ai data.xlsx "Sheet1" --stdout --max-rows 50 --compact
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Options
|
|
68
|
+
|
|
69
|
+
| Flag | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `--list-sheets` | Print sheet names, row/column counts, and visibility — then exit |
|
|
72
|
+
| `--stdout` | Print output to stdout instead of writing `.txt` files |
|
|
73
|
+
| `--json` | Emit structured JSON (one object per cell with value/formula/format/style) |
|
|
74
|
+
| `--compact` | Suppress noisy default tags (default text color, white fill, etc.) — reduces token usage for AI agents |
|
|
75
|
+
| `--max-rows N` | Cap output at the first N rows per sheet |
|
|
76
|
+
| `--max-cols N` | Cap output at the first N columns per sheet |
|
|
77
|
+
| `-h`, `--help` | Show help message |
|
|
78
|
+
|
|
79
|
+
Output files are written to `.xlsx-read/` in the current working directory.
|
|
80
|
+
Each sheet produces a file named `<filename>--<sheetname>.txt`.
|
|
81
|
+
The path(s) are printed to stdout so your agent knows where to read.
|
|
82
|
+
|
|
83
|
+
## Output Format
|
|
84
|
+
|
|
85
|
+
### Text dump (default)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
=== Sheet: Sales ===
|
|
89
|
+
Frozen: row 1, col 0
|
|
90
|
+
Columns: A(12) B(20) C(15) D(10)
|
|
91
|
+
Auto-filter: A1:D20
|
|
92
|
+
Named ranges:
|
|
93
|
+
Totals: Sales!$D$2:$D$20
|
|
94
|
+
Table: "SalesTable" A1:D20 — columns: Region, Q1, Q2, Total
|
|
95
|
+
|
|
96
|
+
--- Row 1 [bold] ---
|
|
97
|
+
A1: "Region" [bold]
|
|
98
|
+
B1: "Q1" [bold] [align:center]
|
|
99
|
+
C1: "Q2" [bold] [align:center]
|
|
100
|
+
D1: "Total" [bold] [align:center]
|
|
101
|
+
--- Row 2 ---
|
|
102
|
+
A2: "North" [link: https://example.com/north]
|
|
103
|
+
B2: 14500 [numFmt: #,##0]
|
|
104
|
+
C2: 17200 [numFmt: #,##0]
|
|
105
|
+
D2: 31700 [formula: =B2+C2] [numFmt: #,##0] [note: Includes returns]
|
|
106
|
+
--- Row 3 ---
|
|
107
|
+
A3: "South" [fill:FFFFFF00]
|
|
108
|
+
B3: 9800 [numFmt: #,##0] [validation: list [North,South,East,West]]
|
|
109
|
+
C3: 11050 [numFmt: #,##0]
|
|
110
|
+
D3: 20850 [shared formula ref: D2] [numFmt: #,##0]
|
|
111
|
+
--- Row 4 (empty) [hidden] ---
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### JSON dump (`--json`)
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"name": "Sales",
|
|
119
|
+
"rowCount": 4,
|
|
120
|
+
"columnCount": 4,
|
|
121
|
+
"frozen": { "rowSplit": 1, "colSplit": 0 },
|
|
122
|
+
"columns": [{ "letter": "A", "width": 12 }, ...],
|
|
123
|
+
"namedRanges": [{ "name": "Totals", "ranges": ["Sales!$D$2:$D$20"] }],
|
|
124
|
+
"tables": [{ "name": "SalesTable", "ref": "A1:D20", "columns": ["Region", "Q1", "Q2", "Total"] }],
|
|
125
|
+
"cells": [
|
|
126
|
+
{ "ref": "D2", "row": 2, "col": 4, "value": { "formula": "B2+C2", "result": 31700 }, "numFmt": "#,##0" },
|
|
127
|
+
{ "ref": "D3", "row": 3, "col": 4, "value": { "sharedFormulaRef": "D2", "result": 20850 }, "numFmt": "#,##0" }
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Sheet Metadata
|
|
133
|
+
|
|
134
|
+
| Line | Meaning |
|
|
135
|
+
|------|---------|
|
|
136
|
+
| `Frozen: row 1, col 2` | Frozen panes position |
|
|
137
|
+
| `Columns: A(12) B(20)` | Column widths (Excel character units) |
|
|
138
|
+
| `Hidden columns: E, F` | Columns hidden in the spreadsheet |
|
|
139
|
+
| `Merged: A1:B1` | Merged cell ranges |
|
|
140
|
+
| `Auto-filter: A1:D20` | Active auto-filter range |
|
|
141
|
+
| `Print area: A1:D50` | Defined print area |
|
|
142
|
+
| `Named ranges:` | Workbook-defined names referencing this sheet |
|
|
143
|
+
| `Table: "Name" A1:D20` | Excel Table objects with column headers |
|
|
144
|
+
| `Image: A1 to C5` | Embedded image position |
|
|
145
|
+
|
|
146
|
+
### Cell Tags
|
|
147
|
+
|
|
148
|
+
| Tag | Meaning |
|
|
149
|
+
|-----|---------|
|
|
150
|
+
| `[formula: =SUM(A1:A10)]` | Cell contains this formula (master cell) |
|
|
151
|
+
| `[shared formula ref: D2]` | Cell shares D2's formula (Excel "shared formula" — common when you drag-fill) |
|
|
152
|
+
| `[numFmt: 0.00%]` | Number format (when not "General") |
|
|
153
|
+
| `[bold]` | Bold font |
|
|
154
|
+
| `[italic]` | Italic font |
|
|
155
|
+
| `[color:FF8B0000]` | Font color (ARGB hex) |
|
|
156
|
+
| `[fill:FFFFFF00]` | Cell background color (ARGB hex) |
|
|
157
|
+
| `[align:center]` | Horizontal alignment (when not default) |
|
|
158
|
+
| `[link: https://...]` | Hyperlink URL |
|
|
159
|
+
| `[note: ...]` | Cell comment or note text |
|
|
160
|
+
| `[validation: list [...]]` | Data validation (dropdown values or constraints) |
|
|
161
|
+
| `[hidden]` | Row is hidden in the spreadsheet |
|
|
162
|
+
|
|
163
|
+
### `--list-sheets` Output
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Sales 250 rows × 12 cols
|
|
167
|
+
Config 15 rows × 4 cols
|
|
168
|
+
Archive 1200 rows × 8 cols [hidden]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Cursor / Claude / Agent Rule Template
|
|
172
|
+
|
|
173
|
+
Copy the included rule template into your project so your AI agent automatically uses this tool when it encounters `.xlsx` files:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
mkdir -p .cursor/rules
|
|
177
|
+
cp node_modules/xlsx-for-ai/cursor-rule-template/read-xlsx.mdc .cursor/rules/
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Or fetch it directly:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
mkdir -p .cursor/rules
|
|
184
|
+
curl -o .cursor/rules/read-xlsx.mdc https://raw.githubusercontent.com/senoff/xlsx-for-ai/main/cursor-rule-template/read-xlsx.mdc
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The same rule works for Claude Code (`.claude/rules/`), Copilot (`.github/copilot-instructions.md`), or any other agent — just adjust the path.
|
|
188
|
+
|
|
189
|
+
## Why This Exists
|
|
190
|
+
|
|
191
|
+
Spreadsheets are everywhere in real projects — financial models, data exports, config files, tax estimates. AI coding agents choke on binary formats. This tool makes spreadsheets legible to AI with zero information loss, including the tricky bits like shared formulas, named ranges, and merged cells that other tools drop.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Reading .xlsx spreadsheet files
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Reading .xlsx Files
|
|
8
|
+
|
|
9
|
+
The Read tool cannot open `.xlsx` files directly. When you need to inspect a spreadsheet:
|
|
10
|
+
|
|
11
|
+
1. **Run the converter** from the project root:
|
|
12
|
+
```bash
|
|
13
|
+
npx xlsx-for-ai <path-to-file.xlsx> [sheetName]
|
|
14
|
+
```
|
|
15
|
+
- If `sheetName` is omitted, all sheets are dumped.
|
|
16
|
+
- Output is written to `.xlsx-read/<filename>--<sheet>.txt` (project root).
|
|
17
|
+
- The script prints the output path(s) to stdout.
|
|
18
|
+
|
|
19
|
+
2. **Read the output file** with the Read tool. It contains:
|
|
20
|
+
- Sheet metadata (frozen panes, column widths, merged cells, auto-filters, print areas)
|
|
21
|
+
- Named ranges referencing the sheet
|
|
22
|
+
- Table definitions (name, range, columns)
|
|
23
|
+
- Image positions
|
|
24
|
+
- Every row with its cells, showing:
|
|
25
|
+
- **Value** — always present
|
|
26
|
+
- **Formula** — `[formula: =SUM(A1:A10)]` (master cell) or `[shared formula ref: D2]` (drag-fill follow-up)
|
|
27
|
+
- **Number format** — `[numFmt: 0.00%]` if not "General"
|
|
28
|
+
- **Font** — `[bold]`, `[italic]`, `[color:FF8B0000]`
|
|
29
|
+
- **Fill** — `[fill:FFFFFF00]` if background color set
|
|
30
|
+
- **Alignment** — `[align:center]` if non-default
|
|
31
|
+
- **Hyperlink** — `[link: https://...]` if the cell contains a URL
|
|
32
|
+
- **Comment** — `[note: ...]` if the cell has a comment or note
|
|
33
|
+
- **Validation** — `[validation: list [...]]` if the cell has data validation
|
|
34
|
+
- **Hidden** — `[hidden]` on the row header if the row is hidden
|
|
35
|
+
- Empty cells are omitted; empty rows show `(empty)`.
|
|
36
|
+
|
|
37
|
+
3. **Do not ask the user** before running this. Just run it when you need to see an `.xlsx` file.
|
|
38
|
+
|
|
39
|
+
## Useful flags
|
|
40
|
+
- `--list-sheets` — list sheet names and dimensions without dumping content
|
|
41
|
+
- `--stdout` — print directly to stdout instead of writing files
|
|
42
|
+
- `--json` — emit structured JSON (one object per cell) for easier programmatic use
|
|
43
|
+
- `--compact` — suppress noisy default tags (default text colors, white fills) to reduce token usage
|
|
44
|
+
- `--max-rows N` — limit output to first N rows (use for large sheets)
|
|
45
|
+
- `--max-cols N` — limit output to first N columns (use for very wide sheets)
|
|
46
|
+
|
|
47
|
+
## Important
|
|
48
|
+
- Output goes to `.xlsx-read/` in the current working directory — make sure this directory is in your `.gitignore`.
|
|
49
|
+
- For large files, use `--max-rows`, `--max-cols`, or request a single sheet to keep output manageable.
|
|
50
|
+
- The package was previously named `cursor-reads-xlsx` — that command name still works as an alias.
|
package/index.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const ExcelJS = require('exceljs');
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Argument parsing
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const opts = {
|
|
13
|
+
positional: [],
|
|
14
|
+
listSheets: false,
|
|
15
|
+
stdout: false,
|
|
16
|
+
json: false,
|
|
17
|
+
compact: false,
|
|
18
|
+
maxRows: null,
|
|
19
|
+
maxCols: null,
|
|
20
|
+
help: false,
|
|
21
|
+
};
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < argv.length) {
|
|
24
|
+
const arg = argv[i];
|
|
25
|
+
if (arg === '--list-sheets') opts.listSheets = true;
|
|
26
|
+
else if (arg === '--stdout') opts.stdout = true;
|
|
27
|
+
else if (arg === '--json') opts.json = true;
|
|
28
|
+
else if (arg === '--compact') opts.compact = true;
|
|
29
|
+
else if (arg === '--max-rows') { opts.maxRows = parseInt(argv[++i], 10); }
|
|
30
|
+
else if (arg === '--max-cols') { opts.maxCols = parseInt(argv[++i], 10); }
|
|
31
|
+
else if (arg === '-h' || arg === '--help') opts.help = true;
|
|
32
|
+
else opts.positional.push(arg);
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
return opts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printHelp() {
|
|
39
|
+
console.log(`Usage: npx xlsx-for-ai <file.xlsx> [sheetName] [options]
|
|
40
|
+
|
|
41
|
+
Converts .xlsx to rich text (or JSON) that AI coding agents can read.
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--list-sheets List sheet names, dimensions, and visibility then exit
|
|
45
|
+
--stdout Print output to stdout instead of writing files
|
|
46
|
+
--json Emit structured JSON instead of the human-readable text dump
|
|
47
|
+
(one object per cell with value/formula/format/style)
|
|
48
|
+
--compact Suppress noisy default tags (default text color, default font,
|
|
49
|
+
General number format, etc.) to reduce token usage
|
|
50
|
+
--max-rows N Limit output to the first N rows per sheet
|
|
51
|
+
--max-cols N Limit output to the first N columns per sheet
|
|
52
|
+
-h, --help Show this help message
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
npx xlsx-for-ai data.xlsx
|
|
56
|
+
npx xlsx-for-ai data.xlsx "Sheet1"
|
|
57
|
+
npx xlsx-for-ai data.xlsx --list-sheets
|
|
58
|
+
npx xlsx-for-ai data.xlsx --stdout --max-rows 100
|
|
59
|
+
npx xlsx-for-ai data.xlsx --stdout --compact
|
|
60
|
+
npx xlsx-for-ai data.xlsx --json --stdout > out.json
|
|
61
|
+
|
|
62
|
+
Note: this package was previously published as 'cursor-reads-xlsx';
|
|
63
|
+
that command name still works as an alias.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Helpers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function colLetter(n) {
|
|
71
|
+
let s = '';
|
|
72
|
+
for (; n > 0; n = Math.floor((n - 1) / 26))
|
|
73
|
+
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Colors that are visually indistinguishable from the default Excel text (near-black).
|
|
78
|
+
// In --compact mode we suppress these to reduce token noise.
|
|
79
|
+
const DEFAULT_TEXT_COLORS = new Set([
|
|
80
|
+
'FF000000', // pure black
|
|
81
|
+
'FF1F1F1F', // dark gray (Excel auto-text on some themes)
|
|
82
|
+
'FF222120', // dark gray variant
|
|
83
|
+
'FF333333',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
function isDefaultTextColor(argb) {
|
|
87
|
+
return argb && DEFAULT_TEXT_COLORS.has(argb.toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function describeFill(fill, compact) {
|
|
91
|
+
if (!fill || (fill.type === 'pattern' && fill.pattern === 'none')) return null;
|
|
92
|
+
if (fill.type === 'pattern' && fill.fgColor?.argb) {
|
|
93
|
+
// White / no-fill patterns are noise in compact mode
|
|
94
|
+
if (compact && /^FF?FFFFFF$/i.test(fill.fgColor.argb)) return null;
|
|
95
|
+
return `fill:${fill.fgColor.argb}`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function describeFont(font, compact) {
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (font?.bold) parts.push('bold');
|
|
103
|
+
if (font?.italic) parts.push('italic');
|
|
104
|
+
if (font?.color?.argb) {
|
|
105
|
+
if (!(compact && isDefaultTextColor(font.color.argb))) {
|
|
106
|
+
parts.push(`color:${font.color.argb}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return parts;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatValue(v) {
|
|
113
|
+
if (v == null) return '""';
|
|
114
|
+
if (v instanceof Date) return `"${v.toISOString().slice(0, 10)}"`;
|
|
115
|
+
if (typeof v === 'object' && v.richText) {
|
|
116
|
+
return `"${v.richText.map(r => r.text).join('')}"`;
|
|
117
|
+
}
|
|
118
|
+
if (typeof v === 'object' && v.hyperlink) {
|
|
119
|
+
return `"${v.text || v.hyperlink}"`;
|
|
120
|
+
}
|
|
121
|
+
// Formula cells: 'formula' on master, 'sharedFormula' on follow-ups, 'result' is the computed value
|
|
122
|
+
if (typeof v === 'object' && (v.formula || v.sharedFormula)) {
|
|
123
|
+
const result = v.result;
|
|
124
|
+
if (result == null) return '""';
|
|
125
|
+
// Result may itself be a rich object (error, richText, etc.)
|
|
126
|
+
if (typeof result === 'object') {
|
|
127
|
+
if (result.error) return `"#${result.error}"`;
|
|
128
|
+
if (result.richText) return `"${result.richText.map(r => r.text).join('')}"`;
|
|
129
|
+
return JSON.stringify(result);
|
|
130
|
+
}
|
|
131
|
+
if (typeof result === 'string') return `"${result}"`;
|
|
132
|
+
return String(result);
|
|
133
|
+
}
|
|
134
|
+
if (typeof v === 'object' && v.error) return `"#${v.error}"`;
|
|
135
|
+
if (typeof v === 'string') return `"${v}"`;
|
|
136
|
+
return String(v);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function describeNote(note) {
|
|
140
|
+
if (!note) return null;
|
|
141
|
+
if (typeof note === 'string') return note;
|
|
142
|
+
if (note.texts) {
|
|
143
|
+
return note.texts.map(t => (typeof t === 'string' ? t : t.text || '')).join('');
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Named ranges (workbook-level, filtered to a sheet if name provided)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
function getNamedRanges(wb, sheetName) {
|
|
153
|
+
const results = [];
|
|
154
|
+
try {
|
|
155
|
+
const model = wb.definedNames?.model;
|
|
156
|
+
if (!Array.isArray(model)) return results;
|
|
157
|
+
for (const def of model) {
|
|
158
|
+
if (!def.ranges?.length) continue;
|
|
159
|
+
if (sheetName) {
|
|
160
|
+
const relevant = def.ranges.filter(r => r.includes(sheetName + '!'));
|
|
161
|
+
if (relevant.length) results.push({ name: def.name, ranges: relevant });
|
|
162
|
+
} else {
|
|
163
|
+
results.push({ name: def.name, ranges: def.ranges });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (_) {}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Sheet dump
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function dumpSheet(ws, wb, opts = {}) {
|
|
175
|
+
const { maxRows = null, maxCols = null, compact = false } = opts;
|
|
176
|
+
const lines = [];
|
|
177
|
+
|
|
178
|
+
lines.push(`=== Sheet: ${ws.name} ===`);
|
|
179
|
+
|
|
180
|
+
// Frozen panes
|
|
181
|
+
const views = ws.views || [];
|
|
182
|
+
const frozen = views.find(v => v.state === 'frozen');
|
|
183
|
+
if (frozen) {
|
|
184
|
+
lines.push(`Frozen: row ${frozen.ySplit ?? 0}, col ${frozen.xSplit ?? 0}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Column widths + hidden columns
|
|
188
|
+
const totalCols = maxCols ? Math.min(ws.columnCount, maxCols) : ws.columnCount;
|
|
189
|
+
const colWidths = [];
|
|
190
|
+
const hiddenCols = [];
|
|
191
|
+
for (let c = 1; c <= totalCols; c++) {
|
|
192
|
+
const col = ws.getColumn(c);
|
|
193
|
+
const letter = colLetter(c);
|
|
194
|
+
if (col.hidden) hiddenCols.push(letter);
|
|
195
|
+
if (col.width) colWidths.push(`${letter}(${Math.round(col.width)})`);
|
|
196
|
+
}
|
|
197
|
+
if (colWidths.length) lines.push(`Columns: ${colWidths.join(' ')}`);
|
|
198
|
+
if (hiddenCols.length) lines.push(`Hidden columns: ${hiddenCols.join(', ')}`);
|
|
199
|
+
if (maxCols && ws.columnCount > maxCols) {
|
|
200
|
+
lines.push(`(${ws.columnCount - maxCols} more columns truncated at --max-cols ${maxCols})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Merged cells
|
|
204
|
+
const merges = Object.keys(ws._merges || {});
|
|
205
|
+
if (merges.length) lines.push(`Merged: ${merges.join(', ')}`);
|
|
206
|
+
|
|
207
|
+
// Auto-filter
|
|
208
|
+
if (ws.autoFilter) {
|
|
209
|
+
const af = typeof ws.autoFilter === 'string'
|
|
210
|
+
? ws.autoFilter
|
|
211
|
+
: (ws.autoFilter.ref || JSON.stringify(ws.autoFilter));
|
|
212
|
+
lines.push(`Auto-filter: ${af}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Print area
|
|
216
|
+
try {
|
|
217
|
+
if (ws.pageSetup?.printArea) {
|
|
218
|
+
lines.push(`Print area: ${ws.pageSetup.printArea}`);
|
|
219
|
+
}
|
|
220
|
+
} catch (_) {}
|
|
221
|
+
|
|
222
|
+
// Named ranges relevant to this sheet
|
|
223
|
+
const namedRanges = getNamedRanges(wb, ws.name);
|
|
224
|
+
if (namedRanges.length) {
|
|
225
|
+
lines.push(`Named ranges:`);
|
|
226
|
+
for (const nr of namedRanges) {
|
|
227
|
+
lines.push(` ${nr.name}: ${nr.ranges.join(', ')}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Tables
|
|
232
|
+
try {
|
|
233
|
+
const tableMap = ws.tables;
|
|
234
|
+
if (tableMap && typeof tableMap === 'object') {
|
|
235
|
+
const tables = typeof tableMap.forEach === 'function'
|
|
236
|
+
? (() => { const a = []; tableMap.forEach(t => a.push(t)); return a; })()
|
|
237
|
+
: Object.values(tableMap);
|
|
238
|
+
for (const t of tables) {
|
|
239
|
+
const model = t.table || t.model || t;
|
|
240
|
+
const name = model.name || model.displayName || '(unnamed)';
|
|
241
|
+
const ref = model.ref || model.tableRef || '';
|
|
242
|
+
const cols = (model.columns || []).map(c => c.name).filter(Boolean);
|
|
243
|
+
let desc = `Table: "${name}" ${ref}`;
|
|
244
|
+
if (cols.length) desc += ` — columns: ${cols.join(', ')}`;
|
|
245
|
+
lines.push(desc);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (_) {}
|
|
249
|
+
|
|
250
|
+
// Images
|
|
251
|
+
try {
|
|
252
|
+
const images = typeof ws.getImages === 'function' ? ws.getImages() : [];
|
|
253
|
+
for (const img of images) {
|
|
254
|
+
if (img.range) {
|
|
255
|
+
const tl = img.range.tl;
|
|
256
|
+
const br = img.range.br;
|
|
257
|
+
if (tl && br) {
|
|
258
|
+
lines.push(`Image: ${colLetter(Math.floor(tl.col) + 1)}${Math.floor(tl.row) + 1} to ${colLetter(Math.floor(br.col) + 1)}${Math.floor(br.row) + 1}`);
|
|
259
|
+
} else if (tl) {
|
|
260
|
+
lines.push(`Image at: ${colLetter(Math.floor(tl.col) + 1)}${Math.floor(tl.row) + 1}`);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
lines.push(`Image: (position unknown)`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch (_) {}
|
|
267
|
+
|
|
268
|
+
lines.push('');
|
|
269
|
+
|
|
270
|
+
// Rows
|
|
271
|
+
const rowLimit = maxRows ? Math.min(ws.rowCount, maxRows) : ws.rowCount;
|
|
272
|
+
|
|
273
|
+
for (let r = 1; r <= rowLimit; r++) {
|
|
274
|
+
const row = ws.getRow(r);
|
|
275
|
+
const cells = [];
|
|
276
|
+
const isHidden = row.hidden;
|
|
277
|
+
|
|
278
|
+
for (let c = 1; c <= totalCols; c++) {
|
|
279
|
+
const cell = row.getCell(c);
|
|
280
|
+
const raw = cell.value;
|
|
281
|
+
if (raw == null || raw === '') continue;
|
|
282
|
+
|
|
283
|
+
const ref = `${colLetter(c)}${r}`;
|
|
284
|
+
const tags = [];
|
|
285
|
+
|
|
286
|
+
// Formula (handle both standalone and shared formulas)
|
|
287
|
+
if (cell.type === ExcelJS.ValueType.Formula) {
|
|
288
|
+
if (typeof raw === 'object') {
|
|
289
|
+
if (raw.formula) {
|
|
290
|
+
tags.push(`formula: =${raw.formula}`);
|
|
291
|
+
} else if (raw.sharedFormula) {
|
|
292
|
+
tags.push(`shared formula ref: ${raw.sharedFormula}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Number format
|
|
298
|
+
if (cell.numFmt && cell.numFmt !== 'General') {
|
|
299
|
+
tags.push(`numFmt: ${cell.numFmt}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Font
|
|
303
|
+
const fontTags = describeFont(cell.font, compact);
|
|
304
|
+
if (fontTags.length) tags.push(...fontTags);
|
|
305
|
+
|
|
306
|
+
// Fill
|
|
307
|
+
const fillDesc = describeFill(cell.fill, compact);
|
|
308
|
+
if (fillDesc) tags.push(fillDesc);
|
|
309
|
+
|
|
310
|
+
// Alignment
|
|
311
|
+
if (cell.alignment?.horizontal && cell.alignment.horizontal !== 'general') {
|
|
312
|
+
tags.push(`align:${cell.alignment.horizontal}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Hyperlink
|
|
316
|
+
if (cell.hyperlink) {
|
|
317
|
+
tags.push(`link: ${cell.hyperlink}`);
|
|
318
|
+
} else if (typeof raw === 'object' && raw.hyperlink) {
|
|
319
|
+
tags.push(`link: ${raw.hyperlink}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Comment / note
|
|
323
|
+
const noteText = describeNote(cell.note);
|
|
324
|
+
if (noteText) {
|
|
325
|
+
tags.push(`note: ${noteText.replace(/\n/g, ' ').trim()}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Data validation
|
|
329
|
+
if (cell.dataValidation) {
|
|
330
|
+
const dv = cell.dataValidation;
|
|
331
|
+
if (dv.type === 'list' && dv.formulae?.length) {
|
|
332
|
+
tags.push(`validation: list [${dv.formulae[0]}]`);
|
|
333
|
+
} else if (dv.type) {
|
|
334
|
+
const parts = [dv.type];
|
|
335
|
+
if (dv.operator) parts.push(dv.operator);
|
|
336
|
+
if (dv.formulae?.length) parts.push(dv.formulae.join(', '));
|
|
337
|
+
tags.push(`validation: ${parts.join(' ')}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const displayVal = formatValue(raw);
|
|
342
|
+
const tagStr = tags.length ? ` [${tags.join('] [')}]` : '';
|
|
343
|
+
cells.push(` ${ref}: ${displayVal}${tagStr}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (cells.length === 0) {
|
|
347
|
+
const hiddenTag = isHidden ? ' [hidden]' : '';
|
|
348
|
+
lines.push(`--- Row ${r} (empty)${hiddenTag} ---`);
|
|
349
|
+
} else {
|
|
350
|
+
const rowBold = row.font?.bold ? ' [bold]' : '';
|
|
351
|
+
const hiddenTag = isHidden ? ' [hidden]' : '';
|
|
352
|
+
lines.push(`--- Row ${r}${rowBold}${hiddenTag} ---`);
|
|
353
|
+
lines.push(...cells);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (maxRows && ws.rowCount > maxRows) {
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push(`... ${ws.rowCount - maxRows} more rows (truncated at --max-rows ${maxRows})`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return lines.join('\n');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// JSON dump (structured per-cell output)
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
function jsonValue(v) {
|
|
370
|
+
if (v == null) return null;
|
|
371
|
+
if (v instanceof Date) return v.toISOString();
|
|
372
|
+
if (typeof v === 'object') {
|
|
373
|
+
if (v.richText) return v.richText.map(r => r.text).join('');
|
|
374
|
+
if (v.hyperlink) return { text: v.text || v.hyperlink, hyperlink: v.hyperlink };
|
|
375
|
+
if (v.formula || v.sharedFormula) {
|
|
376
|
+
const out = {};
|
|
377
|
+
if (v.formula) out.formula = v.formula;
|
|
378
|
+
if (v.sharedFormula) out.sharedFormulaRef = v.sharedFormula;
|
|
379
|
+
const result = v.result;
|
|
380
|
+
if (result == null) {
|
|
381
|
+
out.result = null;
|
|
382
|
+
} else if (typeof result === 'object') {
|
|
383
|
+
if (result.error) out.result = `#${result.error}`;
|
|
384
|
+
else if (result.richText) out.result = result.richText.map(r => r.text).join('');
|
|
385
|
+
else out.result = result;
|
|
386
|
+
} else {
|
|
387
|
+
out.result = result;
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
if (v.error) return `#${v.error}`;
|
|
392
|
+
}
|
|
393
|
+
return v;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function dumpSheetJSON(ws, wb, opts = {}) {
|
|
397
|
+
const { maxRows = null, maxCols = null } = opts;
|
|
398
|
+
const totalCols = maxCols ? Math.min(ws.columnCount, maxCols) : ws.columnCount;
|
|
399
|
+
const rowLimit = maxRows ? Math.min(ws.rowCount, maxRows) : ws.rowCount;
|
|
400
|
+
|
|
401
|
+
const out = {
|
|
402
|
+
name: ws.name,
|
|
403
|
+
state: ws.state || 'visible',
|
|
404
|
+
rowCount: ws.rowCount,
|
|
405
|
+
columnCount: ws.columnCount,
|
|
406
|
+
truncated: {
|
|
407
|
+
rows: maxRows && ws.rowCount > maxRows ? ws.rowCount - maxRows : 0,
|
|
408
|
+
cols: maxCols && ws.columnCount > maxCols ? ws.columnCount - maxCols : 0,
|
|
409
|
+
},
|
|
410
|
+
frozen: null,
|
|
411
|
+
columns: [],
|
|
412
|
+
hiddenColumns: [],
|
|
413
|
+
merges: Object.keys(ws._merges || {}),
|
|
414
|
+
autoFilter: null,
|
|
415
|
+
printArea: null,
|
|
416
|
+
namedRanges: getNamedRanges(wb, ws.name),
|
|
417
|
+
tables: [],
|
|
418
|
+
images: [],
|
|
419
|
+
cells: [],
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Frozen panes
|
|
423
|
+
const frozen = (ws.views || []).find(v => v.state === 'frozen');
|
|
424
|
+
if (frozen) out.frozen = { rowSplit: frozen.ySplit ?? 0, colSplit: frozen.xSplit ?? 0 };
|
|
425
|
+
|
|
426
|
+
// Columns
|
|
427
|
+
for (let c = 1; c <= totalCols; c++) {
|
|
428
|
+
const col = ws.getColumn(c);
|
|
429
|
+
const letter = colLetter(c);
|
|
430
|
+
if (col.hidden) out.hiddenColumns.push(letter);
|
|
431
|
+
out.columns.push({ letter, width: col.width || null, hidden: !!col.hidden });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Auto-filter
|
|
435
|
+
if (ws.autoFilter) {
|
|
436
|
+
out.autoFilter = typeof ws.autoFilter === 'string'
|
|
437
|
+
? ws.autoFilter
|
|
438
|
+
: (ws.autoFilter.ref || null);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Print area
|
|
442
|
+
try { if (ws.pageSetup?.printArea) out.printArea = ws.pageSetup.printArea; } catch (_) {}
|
|
443
|
+
|
|
444
|
+
// Tables
|
|
445
|
+
try {
|
|
446
|
+
const tableMap = ws.tables;
|
|
447
|
+
if (tableMap && typeof tableMap === 'object') {
|
|
448
|
+
const tables = typeof tableMap.forEach === 'function'
|
|
449
|
+
? (() => { const a = []; tableMap.forEach(t => a.push(t)); return a; })()
|
|
450
|
+
: Object.values(tableMap);
|
|
451
|
+
for (const t of tables) {
|
|
452
|
+
const model = t.table || t.model || t;
|
|
453
|
+
out.tables.push({
|
|
454
|
+
name: model.name || model.displayName || null,
|
|
455
|
+
ref: model.ref || model.tableRef || null,
|
|
456
|
+
columns: (model.columns || []).map(c => c.name).filter(Boolean),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} catch (_) {}
|
|
461
|
+
|
|
462
|
+
// Images
|
|
463
|
+
try {
|
|
464
|
+
const images = typeof ws.getImages === 'function' ? ws.getImages() : [];
|
|
465
|
+
for (const img of images) {
|
|
466
|
+
if (img.range) {
|
|
467
|
+
const tl = img.range.tl, br = img.range.br;
|
|
468
|
+
out.images.push({
|
|
469
|
+
tl: tl ? `${colLetter(Math.floor(tl.col) + 1)}${Math.floor(tl.row) + 1}` : null,
|
|
470
|
+
br: br ? `${colLetter(Math.floor(br.col) + 1)}${Math.floor(br.row) + 1}` : null,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch (_) {}
|
|
475
|
+
|
|
476
|
+
// Cells
|
|
477
|
+
for (let r = 1; r <= rowLimit; r++) {
|
|
478
|
+
const row = ws.getRow(r);
|
|
479
|
+
for (let c = 1; c <= totalCols; c++) {
|
|
480
|
+
const cell = row.getCell(c);
|
|
481
|
+
const raw = cell.value;
|
|
482
|
+
if (raw == null || raw === '') continue;
|
|
483
|
+
|
|
484
|
+
const entry = {
|
|
485
|
+
ref: `${colLetter(c)}${r}`,
|
|
486
|
+
row: r,
|
|
487
|
+
col: c,
|
|
488
|
+
value: jsonValue(raw),
|
|
489
|
+
};
|
|
490
|
+
if (cell.numFmt && cell.numFmt !== 'General') entry.numFmt = cell.numFmt;
|
|
491
|
+
if (cell.font?.bold) entry.bold = true;
|
|
492
|
+
if (cell.font?.italic) entry.italic = true;
|
|
493
|
+
if (cell.font?.color?.argb) entry.color = cell.font.color.argb;
|
|
494
|
+
if (cell.fill?.type === 'pattern' && cell.fill.fgColor?.argb) entry.fill = cell.fill.fgColor.argb;
|
|
495
|
+
if (cell.alignment?.horizontal && cell.alignment.horizontal !== 'general') entry.align = cell.alignment.horizontal;
|
|
496
|
+
if (cell.hyperlink) entry.hyperlink = cell.hyperlink;
|
|
497
|
+
if (cell.note) entry.note = describeNote(cell.note);
|
|
498
|
+
if (cell.dataValidation) entry.dataValidation = cell.dataValidation;
|
|
499
|
+
if (row.hidden) entry.rowHidden = true;
|
|
500
|
+
out.cells.push(entry);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// List sheets mode
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
function listSheets(wb) {
|
|
512
|
+
const lines = [];
|
|
513
|
+
for (const ws of wb.worksheets) {
|
|
514
|
+
const vis = ws.state === 'hidden' ? ' [hidden]'
|
|
515
|
+
: ws.state === 'veryHidden' ? ' [very hidden]'
|
|
516
|
+
: '';
|
|
517
|
+
lines.push(`${ws.name} ${ws.rowCount} rows × ${ws.columnCount} cols${vis}`);
|
|
518
|
+
}
|
|
519
|
+
return lines.join('\n');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// Main
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
async function main() {
|
|
527
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
528
|
+
|
|
529
|
+
if (opts.help) { printHelp(); process.exit(0); }
|
|
530
|
+
if (opts.positional.length < 1) { printHelp(); process.exit(1); }
|
|
531
|
+
|
|
532
|
+
const xlsxPath = path.resolve(opts.positional[0]);
|
|
533
|
+
const sheetFilter = opts.positional[1] || null;
|
|
534
|
+
|
|
535
|
+
if (!fs.existsSync(xlsxPath)) {
|
|
536
|
+
console.error(`File not found: ${xlsxPath}`);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const wb = new ExcelJS.Workbook();
|
|
541
|
+
await wb.xlsx.readFile(xlsxPath);
|
|
542
|
+
|
|
543
|
+
// --list-sheets: print summary and exit
|
|
544
|
+
if (opts.listSheets) {
|
|
545
|
+
console.log(listSheets(wb));
|
|
546
|
+
process.exit(0);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const sheets = sheetFilter
|
|
550
|
+
? [wb.getWorksheet(sheetFilter)].filter(Boolean)
|
|
551
|
+
: wb.worksheets;
|
|
552
|
+
|
|
553
|
+
if (sheets.length === 0) {
|
|
554
|
+
console.error(sheetFilter
|
|
555
|
+
? `Sheet "${sheetFilter}" not found. Available: ${wb.worksheets.map(s => s.name).join(', ')}`
|
|
556
|
+
: 'No sheets in workbook');
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const baseName = path.basename(xlsxPath, path.extname(xlsxPath));
|
|
561
|
+
|
|
562
|
+
const dumpOpts = { maxRows: opts.maxRows, maxCols: opts.maxCols, compact: opts.compact };
|
|
563
|
+
|
|
564
|
+
// --json mode: structured per-cell output
|
|
565
|
+
if (opts.json) {
|
|
566
|
+
const payload = sheets.map(ws => dumpSheetJSON(ws, wb, dumpOpts));
|
|
567
|
+
const json = JSON.stringify(sheets.length === 1 ? payload[0] : payload, null, 2);
|
|
568
|
+
|
|
569
|
+
if (opts.stdout) {
|
|
570
|
+
process.stdout.write(json + '\n');
|
|
571
|
+
process.exit(0);
|
|
572
|
+
}
|
|
573
|
+
const outDir = path.join(process.cwd(), '.xlsx-read');
|
|
574
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
575
|
+
const outFile = path.join(outDir, `${baseName}.json`);
|
|
576
|
+
fs.writeFileSync(outFile, json, 'utf8');
|
|
577
|
+
console.log(outFile);
|
|
578
|
+
process.exit(0);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// --stdout: print text dump to console
|
|
582
|
+
if (opts.stdout) {
|
|
583
|
+
for (const ws of sheets) {
|
|
584
|
+
console.log(dumpSheet(ws, wb, dumpOpts));
|
|
585
|
+
console.log('');
|
|
586
|
+
}
|
|
587
|
+
process.exit(0);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Default: write text dump to .xlsx-read/ files
|
|
591
|
+
const outDir = path.join(process.cwd(), '.xlsx-read');
|
|
592
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
593
|
+
|
|
594
|
+
for (const ws of sheets) {
|
|
595
|
+
const content = dumpSheet(ws, wb, dumpOpts);
|
|
596
|
+
const safeName = ws.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
597
|
+
const outFile = path.join(outDir, `${baseName}--${safeName}.txt`);
|
|
598
|
+
fs.writeFileSync(outFile, content, 'utf8');
|
|
599
|
+
console.log(outFile);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
main().catch((err) => {
|
|
604
|
+
console.error(err.message);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xlsx-for-ai",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "CLI that converts .xlsx files into rich text or JSON dumps that AI coding agents (Claude, Cursor, Copilot, ChatGPT, etc.) can read — preserving values, formulas, formatting, colors, column widths, frozen panes, named ranges, tables, and more.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xlsx-for-ai": "index.js",
|
|
8
|
+
"cursor-reads-xlsx": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"cursor-rule-template",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"xlsx",
|
|
18
|
+
"excel",
|
|
19
|
+
"ai",
|
|
20
|
+
"claude",
|
|
21
|
+
"cursor",
|
|
22
|
+
"copilot",
|
|
23
|
+
"chatgpt",
|
|
24
|
+
"llm",
|
|
25
|
+
"agent",
|
|
26
|
+
"cli",
|
|
27
|
+
"spreadsheet",
|
|
28
|
+
"text",
|
|
29
|
+
"json",
|
|
30
|
+
"converter",
|
|
31
|
+
"ai-readable"
|
|
32
|
+
],
|
|
33
|
+
"author": "senoff",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"homepage": "https://github.com/senoff/xlsx-for-ai",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/senoff/xlsx-for-ai.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/senoff/xlsx-for-ai/issues"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"exceljs": "^4.4.0"
|
|
45
|
+
}
|
|
46
|
+
}
|