@hasna/skills 0.1.18 → 0.1.20
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 +108 -372
- package/bin/index.js +10455 -9282
- package/bin/mcp.js +4793 -3402
- package/dist/cli/commands/completion.d.ts +5 -0
- package/dist/cli/commands/create-sync-config.d.ts +5 -0
- package/dist/cli/commands/diagnostic.d.ts +5 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/install.d.ts +5 -0
- package/dist/cli/commands/introspect.d.ts +5 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/runtime.d.ts +5 -0
- package/dist/cli/commands/schedule.d.ts +5 -0
- package/dist/index.js +197 -97
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/registry.d.ts +1 -11
- package/dist/lib/scheduler.d.ts +1 -1
- package/dist/lib/scheduler.test.d.ts +4 -0
- package/dist/lib/search.d.ts +17 -0
- package/package.json +3 -3
- package/skills/skill-commitpush/SKILL.md +57 -0
- package/skills/skill-commitpush/package.json +34 -0
- package/skills/skill-commitpush/src/index.ts +34 -0
- package/skills/skill-commitpush/tsconfig.json +17 -0
- package/skills/skill-commitpushpr/SKILL.md +55 -0
- package/skills/skill-commitpushpr/package.json +34 -0
- package/skills/skill-commitpushpr/src/index.ts +34 -0
- package/skills/skill-commitpushpr/tsconfig.json +17 -0
- package/skills/skill-monitor/SKILL.md +69 -0
- package/skills/skill-monitor/package.json +34 -0
- package/skills/skill-monitor/src/index.ts +34 -0
- package/skills/skill-monitor/tsconfig.json +17 -0
- package/skills/skill-read-csv/SKILL.md +62 -0
- package/skills/skill-read-csv/package.json +38 -0
- package/skills/skill-read-csv/src/index.ts +331 -0
- package/skills/skill-read-csv/tsconfig.json +17 -0
- package/skills/skill-read-excel/SKILL.md +64 -0
- package/skills/skill-read-excel/package.json +37 -0
- package/skills/skill-read-excel/src/index.ts +253 -0
- package/skills/skill-read-excel/tsconfig.json +17 -0
- package/skills/skill-read-image/SKILL.md +47 -0
- package/skills/skill-read-image/package.json +34 -0
- package/skills/skill-read-image/src/index.ts +264 -0
- package/skills/skill-read-image/tsconfig.json +17 -0
- package/skills/skill-read-pdf/SKILL.md +52 -0
- package/skills/skill-read-pdf/package.json +37 -0
- package/skills/skill-read-pdf/src/index.ts +376 -0
- package/skills/skill-read-pdf/tsconfig.json +17 -0
- package/skills/skill-tmux-session/SKILL.md +109 -0
- package/skills/skill-tmux-session/package.json +34 -0
- package/skills/skill-tmux-session/src/index.ts +34 -0
- package/skills/skill-tmux-session/tsconfig.json +17 -0
- package/skills/skill-academic-journal-matcher/bin/cli.ts +0 -34
- package/skills/skill-action-item-router/bin/cli.ts +0 -34
- package/skills/skill-ad-creative-generator/bin/cli.ts +0 -34
- package/skills/skill-advanced-math/bin/cli.ts +0 -34
- package/skills/skill-analyze-data/bin/cli.ts +0 -19
- package/skills/skill-anomaly-investigator/bin/cli.ts +0 -34
- package/skills/skill-api-test-suite/bin/cli.ts +0 -34
- package/skills/skill-apidocs/bin/cli.ts +0 -87
- package/skills/skill-audio-cleanup-lab/bin/cli.ts +0 -6
- package/skills/skill-audiobook-chapter-proofer/bin/cli.ts +0 -34
- package/skills/skill-banner-ad-suite/bin/cli.ts +0 -34
- package/skills/skill-benchmark-finder/bin/cli.ts +0 -34
- package/skills/skill-bio-sequence-tool/bin/cli.ts +0 -34
- package/skills/skill-blog-topic-cluster/bin/cli.ts +0 -34
- package/skills/skill-brand-style-guide/bin/cli.ts +0 -19
- package/skills/skill-brand-voice-audit/bin/cli.ts +0 -34
- package/skills/skill-budget-variance-analyzer/bin/cli.ts +0 -6
- package/skills/skill-businessactivity/bin/cli.ts +0 -28
- package/skills/skill-calendar-events/bin/cli.ts +0 -34
- package/skills/skill-campaign-metric-brief/bin/cli.ts +0 -34
- package/skills/skill-campaign-moodboard/bin/cli.ts +0 -34
- package/skills/skill-caption-style-stylist/bin/cli.ts +0 -34
- package/skills/skill-chemistry-calculator/bin/cli.ts +0 -34
- package/skills/skill-churn-risk-notifier/bin/cli.ts +0 -34
- package/skills/skill-citation-formatter/bin/cli.ts +0 -34
- package/skills/skill-classroom-newsletter-kit/bin/cli.ts +0 -34
- package/skills/skill-color-palette-harmonizer/bin/cli.ts +0 -34
- package/skills/skill-competitor-ad-analyzer/bin/cli.ts +0 -34
- package/skills/skill-compliance-copy-check/bin/cli.ts +0 -34
- package/skills/skill-compliance-report-pack/bin/cli.ts +0 -34
- package/skills/skill-compress-video/bin/cli.ts +0 -19
- package/skills/skill-consolelog/bin/cli.ts +0 -884
- package/skills/skill-contract-plainlanguage/bin/cli.ts +0 -34
- package/skills/skill-copytone-translator/bin/cli.ts +0 -34
- package/skills/skill-create-blog-article/bin/cli.ts +0 -34
- package/skills/skill-create-ebook/bin/cli.ts +0 -34
- package/skills/skill-crm-note-enhancer/bin/cli.ts +0 -34
- package/skills/skill-customer-journey-mapper/bin/cli.ts +0 -34
- package/skills/skill-dashboard-builder/bin/cli.ts +0 -34
- package/skills/skill-dashboard-narrator/bin/cli.ts +0 -34
- package/skills/skill-data-anonymizer/bin/cli.ts +0 -34
- package/skills/skill-database-explorer/bin/cli.ts +0 -34
- package/skills/skill-dataset-health-check/bin/cli.ts +0 -34
- package/skills/skill-decision-journal/bin/cli.ts +0 -34
- package/skills/skill-delegation-brief-writer/bin/cli.ts +0 -34
- package/skills/skill-destination-briefing/bin/cli.ts +0 -34
- package/skills/skill-diff-viewer/bin/cli.ts +0 -34
- package/skills/skill-domainpurchase/bin/cli.ts +0 -683
- package/skills/skill-domainsearch/bin/cli.ts +0 -410
- package/skills/skill-educational-resource-finder/bin/cli.ts +0 -34
- package/skills/skill-email-campaign/bin/cli.ts +0 -34
- package/skills/skill-exam-readiness-check/bin/cli.ts +0 -34
- package/skills/skill-experiment-power-calculator/bin/cli.ts +0 -34
- package/skills/skill-extract-audio/bin/cli.ts +0 -19
- package/skills/skill-extract-frames/bin/cli.ts +0 -34
- package/skills/skill-extract-invoice/bin/cli.ts +0 -34
- package/skills/skill-family-activity-curator/bin/cli.ts +0 -34
- package/skills/skill-faq-packager/bin/cli.ts +0 -34
- package/skills/skill-feedback-survey-designer/bin/cli.ts +0 -34
- package/skills/skill-field-trip-planner/bin/cli.ts +0 -34
- package/skills/skill-file-organizer/bin/cli.ts +0 -34
- package/skills/skill-folder-tree/bin/cli.ts +0 -34
- package/skills/skill-forecast-scenario-lab/bin/cli.ts +0 -34
- package/skills/skill-form-filler/bin/cli.ts +0 -34
- package/skills/skill-generate-api-client/bin/cli.ts +0 -34
- package/skills/skill-generate-book-cover/bin/cli.ts +0 -34
- package/skills/skill-generate-chart/bin/cli.ts +0 -34
- package/skills/skill-generate-diagram/bin/cli.ts +0 -34
- package/skills/skill-generate-dockerfile/bin/cli.ts +0 -34
- package/skills/skill-generate-documentation/bin/cli.ts +0 -34
- package/skills/skill-generate-docx/bin/cli.ts +0 -6
- package/skills/skill-generate-env/bin/cli.ts +0 -34
- package/skills/skill-generate-excel/bin/cli.ts +0 -34
- package/skills/skill-generate-favicon/bin/cli.ts +0 -34
- package/skills/skill-generate-mock-data/bin/cli.ts +0 -34
- package/skills/skill-generate-pdf/bin/cli.ts +0 -6
- package/skills/skill-generate-pr-description/bin/cli.ts +0 -34
- package/skills/skill-generate-presentation/bin/cli.ts +0 -34
- package/skills/skill-generate-qrcode/bin/cli.ts +0 -34
- package/skills/skill-generate-regex/bin/cli.ts +0 -34
- package/skills/skill-generate-resume/bin/cli.ts +0 -34
- package/skills/skill-generate-sitemap/bin/cli.ts +0 -34
- package/skills/skill-generate-social-posts/bin/cli.ts +0 -34
- package/skills/skill-generate-sql/bin/cli.ts +0 -34
- package/skills/skill-gif-maker/bin/cli.ts +0 -34
- package/skills/skill-github-manager/bin/cli.ts +0 -34
- package/skills/skill-gmail/bin/cli.ts +0 -34
- package/skills/skill-goal-quarterly-roadmap/bin/cli.ts +0 -34
- package/skills/skill-grant-application-drafter/bin/cli.ts +0 -34
- package/skills/skill-grocery-basket-optimizer/bin/cli.ts +0 -34
- package/skills/skill-guest-communication-suite/bin/cli.ts +0 -34
- package/skills/skill-habit-reflection-digest/bin/cli.ts +0 -34
- package/skills/skill-highlight-reel-generator/bin/cli.ts +0 -34
- package/skills/skill-homework-feedback-coach/bin/cli.ts +0 -34
- package/skills/skill-household-maintenance-mgr/bin/cli.ts +0 -34
- package/skills/skill-http-server/bin/cli.ts +0 -34
- package/skills/skill-implementation-agent/bin/cli.ts +0 -34
- package/skills/skill-implementation-plan/bin/cli.ts +0 -34
- package/skills/skill-implementation-todo/bin/cli.ts +0 -34
- package/skills/skill-inbox-priority-planner/bin/cli.ts +0 -34
- package/skills/skill-invoice/bin/cli.ts +0 -20
- package/skills/skill-invoice-dispute-helper/bin/cli.ts +0 -34
- package/skills/skill-itinerary-architect/bin/cli.ts +0 -34
- package/skills/skill-jingle-composer/bin/cli.ts +0 -34
- package/skills/skill-kpi-digest-generator/bin/cli.ts +0 -34
- package/skills/skill-lab-notebook-formatter/bin/cli.ts +0 -34
- package/skills/skill-landing-page-copy/bin/cli.ts +0 -34
- package/skills/skill-latex-table-generator/bin/cli.ts +0 -34
- package/skills/skill-learning-style-profiler/bin/cli.ts +0 -34
- package/skills/skill-lesson-plan-customizer/bin/cli.ts +0 -34
- package/skills/skill-livestream-runofshow/bin/cli.ts +0 -34
- package/skills/skill-longform-structurer/bin/cli.ts +0 -34
- package/skills/skill-lorem-generator/bin/cli.ts +0 -34
- package/skills/skill-managehook/bin/cli.ts +0 -241
- package/skills/skill-managemcp/bin/cli.ts +0 -241
- package/skills/skill-manageskill/bin/cli.ts +0 -241
- package/skills/skill-markdown-validator/bin/cli.ts +0 -34
- package/skills/skill-mcp-builder/bin/cli.ts +0 -34
- package/skills/skill-meal-plan-designer/bin/cli.ts +0 -34
- package/skills/skill-meeting-insight-summarizer/bin/cli.ts +0 -34
- package/skills/skill-merge-pdfs/bin/cli.ts +0 -34
- package/skills/skill-microcopy-generator/bin/cli.ts +0 -34
- package/skills/skill-mindfulness-prompt-cache/bin/cli.ts +0 -34
- package/skills/skill-notion-manager/bin/cli.ts +0 -34
- package/skills/skill-onboarding-sequence-builder/bin/cli.ts +0 -34
- package/skills/skill-onsite-ops-checklist/bin/cli.ts +0 -34
- package/skills/skill-outreach-cadence-designer/bin/cli.ts +0 -34
- package/skills/skill-packaging-concept-studio/bin/cli.ts +0 -34
- package/skills/skill-packing-plan-pro/bin/cli.ts +0 -34
- package/skills/skill-parent-teacher-brief/bin/cli.ts +0 -34
- package/skills/skill-partner-kit-assembler/bin/cli.ts +0 -34
- package/skills/skill-payroll-change-prepper/bin/cli.ts +0 -34
- package/skills/skill-persona-based-adwriter/bin/cli.ts +0 -34
- package/skills/skill-persona-generator/bin/cli.ts +0 -34
- package/skills/skill-personal-daily-ops/bin/cli.ts +0 -34
- package/skills/skill-pet-care-scheduler/bin/cli.ts +0 -34
- package/skills/skill-podcast-show-notes/bin/cli.ts +0 -34
- package/skills/skill-presentation-theme-maker/bin/cli.ts +0 -34
- package/skills/skill-press-release-drafter/bin/cli.ts +0 -34
- package/skills/skill-print-collateral-designer/bin/cli.ts +0 -34
- package/skills/skill-procurement-scorecard/bin/cli.ts +0 -34
- package/skills/skill-product-demo-script/bin/cli.ts +0 -34
- package/skills/skill-product-mockup/bin/cli.ts +0 -34
- package/skills/skill-project-retro-companion/bin/cli.ts +0 -34
- package/skills/skill-proposal-redline-advisor/bin/cli.ts +0 -34
- package/skills/skill-regex-tester/bin/cli.ts +0 -34
- package/skills/skill-remove-background/bin/cli.ts +0 -34
- package/skills/skill-risk-disclosure-kit/bin/cli.ts +0 -34
- package/skills/skill-roi-comparison-tool/bin/cli.ts +0 -34
- package/skills/skill-sales-call-recapper/bin/cli.ts +0 -34
- package/skills/skill-salescopy/bin/cli.ts +0 -20
- package/skills/skill-scaffold-project/bin/cli.ts +0 -34
- package/skills/skill-scholarship-tracker/bin/cli.ts +0 -34
- package/skills/skill-scientific-figure-check/bin/cli.ts +0 -34
- package/skills/skill-seating-chart-maker/bin/cli.ts +0 -34
- package/skills/skill-security-audit/bin/cli.ts +0 -34
- package/skills/skill-seo-brief-builder/bin/cli.ts +0 -34
- package/skills/skill-slack-assistant/bin/cli.ts +0 -34
- package/skills/skill-sleep-routine-analyzer/bin/cli.ts +0 -34
- package/skills/skill-social-media-kit/bin/cli.ts +0 -34
- package/skills/skill-split-pdf/bin/cli.ts +0 -34
- package/skills/skill-sponsorship-proposal-lab/bin/cli.ts +0 -34
- package/skills/skill-spreadsheet-cleanroom/bin/cli.ts +0 -34
- package/skills/skill-statistical-test-selector/bin/cli.ts +0 -34
- package/skills/skill-stress-relief-playbook/bin/cli.ts +0 -34
- package/skills/skill-study-guide-builder/bin/cli.ts +0 -34
- package/skills/skill-subscription-spend-watcher/bin/cli.ts +0 -34
- package/skills/skill-subtitle/bin/cli.ts +0 -20
- package/skills/skill-survey-insight-extractor/bin/cli.ts +0 -34
- package/skills/skill-terraform-generator/bin/cli.ts +0 -34
- package/skills/skill-testimonial-graphics/bin/cli.ts +0 -34
- package/skills/skill-timesheet/bin/cli.ts +0 -47
- package/skills/skill-travel-budget-balancer/bin/cli.ts +0 -34
- package/skills/skill-validate-config/bin/cli.ts +0 -34
- package/skills/skill-video-cut-suggester/bin/cli.ts +0 -34
- package/skills/skill-video-downloader/bin/cli.ts +0 -34
- package/skills/skill-video-thumbnail/bin/cli.ts +0 -34
- package/skills/skill-voiceover-casting-assistant/bin/cli.ts +0 -34
- package/skills/skill-watermark/bin/cli.ts +0 -34
- package/skills/skill-webcrawling/bin/cli.ts +0 -21
- package/skills/skill-webinar-script-coach/bin/cli.ts +0 -34
- package/skills/skill-wellness-progress-reporter/bin/cli.ts +0 -34
- package/skills/skill-workout-cycle-planner/bin/cli.ts +0 -34
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createReadStream } from "fs";
|
|
4
|
+
import { mkdir, open, readFile, writeFile } from "fs/promises";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
import { parse as createParser } from "csv-parse";
|
|
7
|
+
import { parse as parseCsvSync } from "csv-parse/sync";
|
|
8
|
+
import iconv from "iconv-lite";
|
|
9
|
+
|
|
10
|
+
const VERSION = "0.1.0";
|
|
11
|
+
const SAMPLE_BYTES = 128 * 1024;
|
|
12
|
+
|
|
13
|
+
type HeaderMode = "auto" | "true" | "false";
|
|
14
|
+
|
|
15
|
+
interface CliOptions {
|
|
16
|
+
input?: string;
|
|
17
|
+
delimiter?: string;
|
|
18
|
+
encoding: string;
|
|
19
|
+
headers: HeaderMode;
|
|
20
|
+
limit?: number;
|
|
21
|
+
output?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CsvResult {
|
|
25
|
+
input: string;
|
|
26
|
+
encoding: string;
|
|
27
|
+
delimiter: string;
|
|
28
|
+
hasHeader: boolean;
|
|
29
|
+
columns: string[];
|
|
30
|
+
rowCount: number;
|
|
31
|
+
truncated: boolean;
|
|
32
|
+
rows: Array<Record<string, string | null>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printHelp(): void {
|
|
36
|
+
console.log(`skill-read-csv v${VERSION}
|
|
37
|
+
|
|
38
|
+
USAGE:
|
|
39
|
+
skill-read-csv --input <path> [options]
|
|
40
|
+
|
|
41
|
+
OPTIONS:
|
|
42
|
+
-i, --input <path> CSV file to parse
|
|
43
|
+
-d, --delimiter <value> comma | tab | semicolon | pipe | literal character
|
|
44
|
+
-e, --encoding <value> auto | utf8 | utf16le | utf16be | latin1 | win1252
|
|
45
|
+
--headers <mode> auto | true | false
|
|
46
|
+
-l, --limit <n> Stop after parsing n rows
|
|
47
|
+
-o, --output <path> Save JSON result to a file
|
|
48
|
+
--help Show this help message
|
|
49
|
+
--version Show the current version
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
54
|
+
const options: CliOptions = {
|
|
55
|
+
encoding: "auto",
|
|
56
|
+
headers: "auto",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
+
const arg = argv[i];
|
|
61
|
+
|
|
62
|
+
switch (arg) {
|
|
63
|
+
case "--help":
|
|
64
|
+
case "-h":
|
|
65
|
+
printHelp();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
case "--version":
|
|
68
|
+
case "-v":
|
|
69
|
+
console.log(VERSION);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
case "--input":
|
|
72
|
+
case "-i":
|
|
73
|
+
options.input = argv[++i];
|
|
74
|
+
break;
|
|
75
|
+
case "--delimiter":
|
|
76
|
+
case "-d":
|
|
77
|
+
options.delimiter = argv[++i];
|
|
78
|
+
break;
|
|
79
|
+
case "--encoding":
|
|
80
|
+
case "-e":
|
|
81
|
+
options.encoding = argv[++i] ?? "auto";
|
|
82
|
+
break;
|
|
83
|
+
case "--headers": {
|
|
84
|
+
const value = (argv[++i] ?? "auto").toLowerCase();
|
|
85
|
+
if (value !== "auto" && value !== "true" && value !== "false") {
|
|
86
|
+
throw new Error(`Invalid --headers value: ${value}`);
|
|
87
|
+
}
|
|
88
|
+
options.headers = value;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "--limit":
|
|
92
|
+
case "-l": {
|
|
93
|
+
const value = Number.parseInt(argv[++i] ?? "", 10);
|
|
94
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
95
|
+
throw new Error(`Invalid --limit value: ${argv[i]}`);
|
|
96
|
+
}
|
|
97
|
+
options.limit = value;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "--output":
|
|
101
|
+
case "-o":
|
|
102
|
+
options.output = argv[++i];
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
if (arg.startsWith("-")) {
|
|
106
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
107
|
+
}
|
|
108
|
+
if (!options.input) {
|
|
109
|
+
options.input = arg;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!options.input) {
|
|
117
|
+
throw new Error("Missing required --input <path> argument");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return options;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeDelimiter(input?: string): string | undefined {
|
|
124
|
+
if (!input) return undefined;
|
|
125
|
+
const value = input.toLowerCase();
|
|
126
|
+
switch (value) {
|
|
127
|
+
case "comma":
|
|
128
|
+
return ",";
|
|
129
|
+
case "tab":
|
|
130
|
+
return "\t";
|
|
131
|
+
case "semicolon":
|
|
132
|
+
return ";";
|
|
133
|
+
case "pipe":
|
|
134
|
+
return "|";
|
|
135
|
+
default:
|
|
136
|
+
return input;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readSample(path: string, byteCount = SAMPLE_BYTES): Promise<Buffer> {
|
|
141
|
+
const handle = await open(path, "r");
|
|
142
|
+
try {
|
|
143
|
+
const buffer = Buffer.alloc(byteCount);
|
|
144
|
+
const { bytesRead } = await handle.read(buffer, 0, byteCount, 0);
|
|
145
|
+
return buffer.subarray(0, bytesRead);
|
|
146
|
+
} finally {
|
|
147
|
+
await handle.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function detectEncoding(sample: Buffer, requested: string): { encoding: string; bomBytes: number } {
|
|
152
|
+
if (requested !== "auto") {
|
|
153
|
+
return { encoding: requested, bomBytes: 0 };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (sample.length >= 3 && sample[0] === 0xef && sample[1] === 0xbb && sample[2] === 0xbf) {
|
|
157
|
+
return { encoding: "utf8", bomBytes: 3 };
|
|
158
|
+
}
|
|
159
|
+
if (sample.length >= 2 && sample[0] === 0xff && sample[1] === 0xfe) {
|
|
160
|
+
return { encoding: "utf16le", bomBytes: 2 };
|
|
161
|
+
}
|
|
162
|
+
if (sample.length >= 2 && sample[0] === 0xfe && sample[1] === 0xff) {
|
|
163
|
+
return { encoding: "utf16be", bomBytes: 2 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { encoding: "utf8", bomBytes: 0 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function detectDelimiter(sampleText: string): string {
|
|
170
|
+
const candidates = [",", "\t", ";", "|"];
|
|
171
|
+
const lines = sampleText
|
|
172
|
+
.split(/\r?\n/)
|
|
173
|
+
.map((line) => line.trim())
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.slice(0, 8);
|
|
176
|
+
|
|
177
|
+
if (lines.length === 0) {
|
|
178
|
+
return ",";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let bestDelimiter = ",";
|
|
182
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
183
|
+
|
|
184
|
+
for (const delimiter of candidates) {
|
|
185
|
+
const counts = lines.map((line) => line.split(delimiter).length - 1);
|
|
186
|
+
const total = counts.reduce((sum, count) => sum + count, 0);
|
|
187
|
+
const variance = counts.reduce((sum, count) => sum + Math.abs(count - counts[0]), 0);
|
|
188
|
+
const score = total * 10 - variance;
|
|
189
|
+
if (score > bestScore) {
|
|
190
|
+
bestScore = score;
|
|
191
|
+
bestDelimiter = delimiter;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return bestDelimiter;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseSampleRows(sampleText: string, delimiter: string): string[][] {
|
|
199
|
+
const records = parseCsvSync(sampleText, {
|
|
200
|
+
delimiter,
|
|
201
|
+
relax_column_count: true,
|
|
202
|
+
skip_empty_lines: true,
|
|
203
|
+
to_line: 5,
|
|
204
|
+
}) as string[][];
|
|
205
|
+
return records;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function looksNumeric(value: string): boolean {
|
|
209
|
+
return value.trim() !== "" && !Number.isNaN(Number(value));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeColumnName(value: string, index: number, seen: Set<string>): string {
|
|
213
|
+
const base = value
|
|
214
|
+
.trim()
|
|
215
|
+
.toLowerCase()
|
|
216
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
217
|
+
.replace(/^_+|_+$/g, "") || `column_${index + 1}`;
|
|
218
|
+
|
|
219
|
+
let next = base;
|
|
220
|
+
let suffix = 2;
|
|
221
|
+
while (seen.has(next)) {
|
|
222
|
+
next = `${base}_${suffix}`;
|
|
223
|
+
suffix += 1;
|
|
224
|
+
}
|
|
225
|
+
seen.add(next);
|
|
226
|
+
return next;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function inferHeader(sampleRows: string[][]): boolean {
|
|
230
|
+
if (sampleRows.length < 2) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const first = sampleRows[0];
|
|
235
|
+
const second = sampleRows[1];
|
|
236
|
+
const unique = new Set(first.map((value) => value.trim().toLowerCase()).filter(Boolean));
|
|
237
|
+
const mostlyText = first.filter((value) => !looksNumeric(value)).length >= Math.ceil(first.length / 2);
|
|
238
|
+
const secondHasNumeric = second.some((value) => looksNumeric(value));
|
|
239
|
+
|
|
240
|
+
return unique.size === first.length && mostlyText && secondHasNumeric;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildColumns(sampleRows: string[][], headerMode: HeaderMode): { hasHeader: boolean; columns: string[] } {
|
|
244
|
+
const firstRow = sampleRows[0] ?? [];
|
|
245
|
+
const seen = new Set<string>();
|
|
246
|
+
|
|
247
|
+
if (headerMode === "true" || (headerMode === "auto" && inferHeader(sampleRows))) {
|
|
248
|
+
return {
|
|
249
|
+
hasHeader: true,
|
|
250
|
+
columns: firstRow.map((value, index) => normalizeColumnName(String(value), index, seen)),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
hasHeader: false,
|
|
256
|
+
columns: firstRow.map((_, index) => normalizeColumnName(`column_${index + 1}`, index, seen)),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function parseCsvFile(
|
|
261
|
+
path: string,
|
|
262
|
+
options: CliOptions,
|
|
263
|
+
encoding: string,
|
|
264
|
+
bomBytes: number,
|
|
265
|
+
delimiter: string,
|
|
266
|
+
columns: string[],
|
|
267
|
+
hasHeader: boolean,
|
|
268
|
+
): Promise<{ rows: Array<Record<string, string | null>>; truncated: boolean }> {
|
|
269
|
+
const rows: Array<Record<string, string | null>> = [];
|
|
270
|
+
let truncated = false;
|
|
271
|
+
const stream = createReadStream(path, bomBytes > 0 ? { start: bomBytes } : undefined)
|
|
272
|
+
.pipe(iconv.decodeStream(encoding))
|
|
273
|
+
.pipe(createParser({
|
|
274
|
+
delimiter,
|
|
275
|
+
columns: hasHeader ? true : columns,
|
|
276
|
+
relax_column_count: true,
|
|
277
|
+
skip_empty_lines: true,
|
|
278
|
+
trim: false,
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
for await (const record of stream) {
|
|
282
|
+
rows.push(record as Record<string, string | null>);
|
|
283
|
+
if (options.limit && rows.length >= options.limit) {
|
|
284
|
+
truncated = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
stream.destroy();
|
|
290
|
+
return { rows, truncated };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function writeJson(path: string, payload: CsvResult): Promise<void> {
|
|
294
|
+
await mkdir(dirname(path), { recursive: true });
|
|
295
|
+
await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function main(): Promise<void> {
|
|
299
|
+
const options = parseArgs(process.argv.slice(2));
|
|
300
|
+
const inputPath = resolve(options.input!);
|
|
301
|
+
const sample = await readSample(inputPath);
|
|
302
|
+
const { encoding, bomBytes } = detectEncoding(sample, options.encoding.toLowerCase());
|
|
303
|
+
const sampleText = iconv.decode(sample.subarray(bomBytes), encoding);
|
|
304
|
+
const delimiter = normalizeDelimiter(options.delimiter) ?? detectDelimiter(sampleText);
|
|
305
|
+
const sampleRows = parseSampleRows(sampleText, delimiter);
|
|
306
|
+
const { hasHeader, columns } = buildColumns(sampleRows, options.headers);
|
|
307
|
+
const { rows, truncated } = await parseCsvFile(inputPath, options, encoding, bomBytes, delimiter, columns, hasHeader);
|
|
308
|
+
|
|
309
|
+
const result: CsvResult = {
|
|
310
|
+
input: inputPath,
|
|
311
|
+
encoding,
|
|
312
|
+
delimiter,
|
|
313
|
+
hasHeader,
|
|
314
|
+
columns,
|
|
315
|
+
rowCount: rows.length,
|
|
316
|
+
truncated,
|
|
317
|
+
rows,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (options.output) {
|
|
321
|
+
await writeJson(resolve(options.output), result);
|
|
322
|
+
} else {
|
|
323
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
main().catch((error) => {
|
|
328
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
329
|
+
process.stderr.write(`skill-read-csv: ${message}\n`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"noEmit": true
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
"src/**/*"
|
|
12
|
+
],
|
|
13
|
+
"exclude": [
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-read-excel
|
|
3
|
+
description: Parse XLS and XLSX workbooks into structured JSON with sheet metadata, named ranges, and formatted cell snapshots.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Read Excel
|
|
7
|
+
|
|
8
|
+
Parse Excel workbooks from disk and return structured JSON for one or more sheets.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Supports `.xls` and `.xlsx`
|
|
13
|
+
- Reads multiple sheets in one run
|
|
14
|
+
- Returns workbook metadata, named ranges, and per-sheet rows
|
|
15
|
+
- Preserves sheet order and includes formatted cell snapshots
|
|
16
|
+
- Allows limiting rows for large workbooks
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Parse all sheets
|
|
22
|
+
skill-read-excel --input ./forecast.xlsx
|
|
23
|
+
|
|
24
|
+
# Restrict to specific sheets
|
|
25
|
+
skill-read-excel --input ./ops.xls --sheets Summary,Costs
|
|
26
|
+
|
|
27
|
+
# Limit output and save it
|
|
28
|
+
skill-read-excel --input ./audit.xlsx --limit 100 --output ./audit.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Option | Description | Default |
|
|
34
|
+
|--------|-------------|---------|
|
|
35
|
+
| `-i, --input <path>` | Excel workbook to parse | required |
|
|
36
|
+
| `-s, --sheets <list>` | Comma-separated sheet names to include | all sheets |
|
|
37
|
+
| `-l, --limit <n>` | Limit rows returned per sheet | unlimited |
|
|
38
|
+
| `-o, --output <path>` | Save JSON result to a file | stdout |
|
|
39
|
+
| `--help` | Show usage | |
|
|
40
|
+
| `--version` | Show version | |
|
|
41
|
+
|
|
42
|
+
## Output Shape
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"input": "/absolute/path/to/workbook.xlsx",
|
|
47
|
+
"workbook": {
|
|
48
|
+
"sheetNames": ["Summary", "Data"],
|
|
49
|
+
"namedRanges": []
|
|
50
|
+
},
|
|
51
|
+
"sheets": [
|
|
52
|
+
{
|
|
53
|
+
"name": "Summary",
|
|
54
|
+
"index": 0,
|
|
55
|
+
"rowCount": 12,
|
|
56
|
+
"columnCount": 4,
|
|
57
|
+
"columns": ["month", "revenue", "cost"],
|
|
58
|
+
"rows": [
|
|
59
|
+
{ "month": "Jan", "revenue": 1000, "cost": 600 }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasnaxyz/skill-read-excel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Parse XLS and XLSX workbooks into structured JSON",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skill-read-excel": "src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"SKILL.md",
|
|
13
|
+
"tsconfig.json"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "restricted"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"excel",
|
|
20
|
+
"xlsx",
|
|
21
|
+
"xls",
|
|
22
|
+
"spreadsheet",
|
|
23
|
+
"skill"
|
|
24
|
+
],
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "bun run src/index.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"xlsx": "^0.18.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "latest",
|
|
35
|
+
"typescript": "^5.7.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
|
+
import * as XLSX from "xlsx";
|
|
6
|
+
|
|
7
|
+
const VERSION = "0.1.0";
|
|
8
|
+
|
|
9
|
+
interface CliOptions {
|
|
10
|
+
input?: string;
|
|
11
|
+
sheets?: string[];
|
|
12
|
+
limit?: number;
|
|
13
|
+
output?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface NamedRange {
|
|
17
|
+
name: string;
|
|
18
|
+
ref: string;
|
|
19
|
+
sheet?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SheetResult {
|
|
23
|
+
name: string;
|
|
24
|
+
index: number;
|
|
25
|
+
range: string | null;
|
|
26
|
+
rowCount: number;
|
|
27
|
+
columnCount: number;
|
|
28
|
+
columns: string[];
|
|
29
|
+
rows: Array<Record<string, unknown>>;
|
|
30
|
+
formattedCells: Array<{ cell: string; raw?: unknown; formatted?: string; format?: string }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WorkbookResult {
|
|
34
|
+
input: string;
|
|
35
|
+
workbook: {
|
|
36
|
+
sheetNames: string[];
|
|
37
|
+
namedRanges: NamedRange[];
|
|
38
|
+
};
|
|
39
|
+
sheets: SheetResult[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp(): void {
|
|
43
|
+
console.log(`skill-read-excel v${VERSION}
|
|
44
|
+
|
|
45
|
+
USAGE:
|
|
46
|
+
skill-read-excel --input <path> [options]
|
|
47
|
+
|
|
48
|
+
OPTIONS:
|
|
49
|
+
-i, --input <path> Excel workbook to parse (.xls or .xlsx)
|
|
50
|
+
-s, --sheets <list> Comma-separated sheet names to include
|
|
51
|
+
-l, --limit <n> Limit rows returned per sheet
|
|
52
|
+
-o, --output <path> Save JSON result to a file
|
|
53
|
+
--help Show this help message
|
|
54
|
+
--version Show the current version
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
59
|
+
const options: CliOptions = {};
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
62
|
+
const arg = argv[i];
|
|
63
|
+
switch (arg) {
|
|
64
|
+
case "--help":
|
|
65
|
+
case "-h":
|
|
66
|
+
printHelp();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
case "--version":
|
|
69
|
+
case "-v":
|
|
70
|
+
console.log(VERSION);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
case "--input":
|
|
73
|
+
case "-i":
|
|
74
|
+
options.input = argv[++i];
|
|
75
|
+
break;
|
|
76
|
+
case "--sheets":
|
|
77
|
+
case "-s":
|
|
78
|
+
options.sheets = (argv[++i] ?? "")
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((value) => value.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
break;
|
|
83
|
+
case "--limit":
|
|
84
|
+
case "-l": {
|
|
85
|
+
const value = Number.parseInt(argv[++i] ?? "", 10);
|
|
86
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
87
|
+
throw new Error(`Invalid --limit value: ${argv[i]}`);
|
|
88
|
+
}
|
|
89
|
+
options.limit = value;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "--output":
|
|
93
|
+
case "-o":
|
|
94
|
+
options.output = argv[++i];
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
if (arg.startsWith("-")) {
|
|
98
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
99
|
+
}
|
|
100
|
+
if (!options.input) {
|
|
101
|
+
options.input = arg;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!options.input) {
|
|
109
|
+
throw new Error("Missing required --input <path> argument");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return options;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeColumnName(value: unknown, index: number, seen: Set<string>): string {
|
|
116
|
+
const text = String(value ?? "")
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
120
|
+
.replace(/^_+|_+$/g, "") || `column_${index + 1}`;
|
|
121
|
+
|
|
122
|
+
let next = text;
|
|
123
|
+
let suffix = 2;
|
|
124
|
+
while (seen.has(next)) {
|
|
125
|
+
next = `${text}_${suffix}`;
|
|
126
|
+
suffix += 1;
|
|
127
|
+
}
|
|
128
|
+
seen.add(next);
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function inferHeader(rows: unknown[][]): boolean {
|
|
133
|
+
if (rows.length < 2) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const first = rows[0];
|
|
137
|
+
const second = rows[1];
|
|
138
|
+
const unique = new Set(first.map((value) => String(value ?? "").trim().toLowerCase()).filter(Boolean));
|
|
139
|
+
const mostlyText = first.filter((value) => String(value ?? "").trim() !== "" && Number.isNaN(Number(value))).length >= Math.ceil(first.length / 2);
|
|
140
|
+
const secondHasNumeric = second.some((value) => String(value ?? "").trim() !== "" && !Number.isNaN(Number(value)));
|
|
141
|
+
return unique.size === first.length && mostlyText && secondHasNumeric;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectFormattedCells(sheet: XLSX.WorkSheet, maxCells = 50): Array<{ cell: string; raw?: unknown; formatted?: string; format?: string }> {
|
|
145
|
+
const result: Array<{ cell: string; raw?: unknown; formatted?: string; format?: string }> = [];
|
|
146
|
+
const entries = Object.entries(sheet).filter(([key]) => !key.startsWith("!"));
|
|
147
|
+
|
|
148
|
+
for (const [cell, value] of entries) {
|
|
149
|
+
if (result.length >= maxCells) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
const cellValue = value as XLSX.CellObject;
|
|
153
|
+
if (cellValue.z || cellValue.w) {
|
|
154
|
+
result.push({
|
|
155
|
+
cell,
|
|
156
|
+
raw: cellValue.v,
|
|
157
|
+
formatted: cellValue.w,
|
|
158
|
+
format: typeof cellValue.z === "string" ? cellValue.z : undefined,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseSheet(name: string, index: number, sheet: XLSX.WorkSheet, limit?: number): SheetResult {
|
|
167
|
+
const rows = XLSX.utils.sheet_to_json(sheet, {
|
|
168
|
+
header: 1,
|
|
169
|
+
defval: null,
|
|
170
|
+
raw: false,
|
|
171
|
+
blankrows: false,
|
|
172
|
+
}) as unknown[][];
|
|
173
|
+
|
|
174
|
+
const trimmedRows = limit ? rows.slice(0, limit + 1) : rows;
|
|
175
|
+
const hasHeader = inferHeader(trimmedRows);
|
|
176
|
+
const headerRow = trimmedRows[0] ?? [];
|
|
177
|
+
const seen = new Set<string>();
|
|
178
|
+
const columns = (hasHeader ? headerRow : headerRow.map((_, colIndex) => `column_${colIndex + 1}`))
|
|
179
|
+
.map((value, colIndex) => normalizeColumnName(value, colIndex, seen));
|
|
180
|
+
|
|
181
|
+
const dataRows = hasHeader ? trimmedRows.slice(1) : trimmedRows;
|
|
182
|
+
const normalizedRows = dataRows.map((row) =>
|
|
183
|
+
columns.reduce<Record<string, unknown>>((record, column, colIndex) => {
|
|
184
|
+
record[column] = row[colIndex] ?? null;
|
|
185
|
+
return record;
|
|
186
|
+
}, {})
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
index,
|
|
192
|
+
range: sheet["!ref"] ?? null,
|
|
193
|
+
rowCount: normalizedRows.length,
|
|
194
|
+
columnCount: columns.length,
|
|
195
|
+
columns,
|
|
196
|
+
rows: normalizedRows,
|
|
197
|
+
formattedCells: collectFormattedCells(sheet),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectNamedRanges(workbook: XLSX.WorkBook): NamedRange[] {
|
|
202
|
+
const names = workbook.Workbook?.Names ?? [];
|
|
203
|
+
return names.map((entry) => ({
|
|
204
|
+
name: entry.Name ?? "",
|
|
205
|
+
ref: entry.Ref ?? "",
|
|
206
|
+
sheet: entry.Sheet != null ? workbook.SheetNames[entry.Sheet] : undefined,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function writeJson(path: string, payload: WorkbookResult): Promise<void> {
|
|
211
|
+
await mkdir(dirname(path), { recursive: true });
|
|
212
|
+
await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function main(): Promise<void> {
|
|
216
|
+
const options = parseArgs(process.argv.slice(2));
|
|
217
|
+
const inputPath = resolve(options.input!);
|
|
218
|
+
const workbook = XLSX.readFile(inputPath, {
|
|
219
|
+
cellDates: true,
|
|
220
|
+
cellNF: true,
|
|
221
|
+
cellStyles: true,
|
|
222
|
+
dense: false,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const selectedSheetNames = options.sheets?.length
|
|
226
|
+
? workbook.SheetNames.filter((name) => options.sheets!.includes(name))
|
|
227
|
+
: workbook.SheetNames;
|
|
228
|
+
|
|
229
|
+
if (selectedSheetNames.length === 0) {
|
|
230
|
+
throw new Error("No matching sheets found");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result: WorkbookResult = {
|
|
234
|
+
input: inputPath,
|
|
235
|
+
workbook: {
|
|
236
|
+
sheetNames: workbook.SheetNames,
|
|
237
|
+
namedRanges: collectNamedRanges(workbook),
|
|
238
|
+
},
|
|
239
|
+
sheets: selectedSheetNames.map((name) => parseSheet(name, workbook.SheetNames.indexOf(name), workbook.Sheets[name], options.limit)),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (options.output) {
|
|
243
|
+
await writeJson(resolve(options.output), result);
|
|
244
|
+
} else {
|
|
245
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main().catch((error) => {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
process.stderr.write(`skill-read-excel: ${message}\n`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|