Back to skills
SkillHub ClubWrite Technical DocsFull StackData / AITech Writer

xlsx-generator

Create and manipulate Excel XLSX files programmatically. Use when the user needs to generate spreadsheets, modify XLSX templates, extract spreadsheet content, or automate Excel workflows. Supports both template-based generation (for branding compliance) and from-scratch creation. Keywords: Excel, XLSX, spreadsheet, workbook, worksheet, data, report, template, financial, analysis.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
35
Hot score
90
Updated
March 20, 2026
Overall rating
C3.0
Composite score
3.0
Best-practice grade
N/A

Install command

npx @skill-hub/cli install jwynia-agent-skills-xlsx-generator
Excelautomationdatareportspreadsheet

Repository

jwynia/agent-skills

Skill path: skills/document-processing/spreadsheet/xlsx-generator

Create and manipulate Excel XLSX files programmatically. Use when the user needs to generate spreadsheets, modify XLSX templates, extract spreadsheet content, or automate Excel workflows. Supports both template-based generation (for branding compliance) and from-scratch creation. Keywords: Excel, XLSX, spreadsheet, workbook, worksheet, data, report, template, financial, analysis.

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Data / AI, Tech Writer.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: jwynia.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install xlsx-generator into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/jwynia/agent-skills before adding xlsx-generator to shared team environments
  • Use xlsx-generator for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: xlsx-generator
description: "Create and manipulate Excel XLSX files programmatically. Use when the user needs to generate spreadsheets, modify XLSX templates, extract spreadsheet content, or automate Excel workflows. Supports both template-based generation (for branding compliance) and from-scratch creation. Keywords: Excel, XLSX, spreadsheet, workbook, worksheet, data, report, template, financial, analysis."
license: MIT
compatibility: Requires Deno with --allow-read, --allow-write permissions
metadata:
  author: agent-skills
  version: "1.0"
---

# XLSX Generator

## When to Use This Skill

Use this skill when:
- Creating Excel spreadsheets programmatically from data or specifications
- Populating branded templates with dynamic content while preserving formatting
- Extracting cell data, formulas, and structure from existing XLSX files
- Finding and replacing placeholder text like `{{TITLE}}` or `${date}` in cells
- Automating spreadsheet generation workflows (reports, data exports, financial statements)

Do NOT use this skill when:
- User wants to open/view spreadsheets (use native Excel or viewer)
- Complex pivot tables or charts are required (limited support)
- Working with older .xls format (XLSX only)
- Real-time collaborative editing is needed

## Prerequisites

- Deno installed (https://deno.land/)
- Input XLSX files for template-based operations
- JSON specification for scratch generation

## Quick Start

### Two Modes of Operation

1. **Template Mode**: Modify existing branded templates
   - Analyze template to find placeholders and structure
   - Replace `{{PLACEHOLDERS}}` with actual values

2. **Scratch Mode**: Create spreadsheets from nothing using JSON specifications

## Instructions

### Mode 1: Template-Based Generation

#### Step 1a: Analyze the Template

Extract cell inventory to understand what can be replaced:

```bash
deno run --allow-read scripts/analyze-template.ts financial-template.xlsx > inventory.json
```

**Output** (inventory.json):
```json
{
  "filename": "financial-template.xlsx",
  "sheetCount": 3,
  "sheets": [
    {
      "name": "Summary",
      "rowCount": 25,
      "colCount": 8,
      "usedRange": "A1:H25",
      "cells": [
        { "address": "A1", "row": 1, "col": 1, "value": "{{REPORT_TITLE}}", "type": "string" },
        { "address": "B3", "row": 3, "col": 2, "value": "{{DATE}}", "type": "string" },
        { "address": "C5", "row": 5, "col": 3, "value": null, "type": "number", "formula": "SUM(C6:C20)" }
      ]
    }
  ],
  "placeholders": [
    { "tag": "{{REPORT_TITLE}}", "location": "Summary!A1", "sheet": "Summary", "address": "A1" },
    { "tag": "{{DATE}}", "location": "Summary!B3", "sheet": "Summary", "address": "B3" }
  ],
  "hasFormulas": true
}
```

#### Step 1b: Create Replacement Specification

Create `replacements.json`:
```json
{
  "textReplacements": [
    { "tag": "{{REPORT_TITLE}}", "value": "Q4 2024 Financial Report" },
    { "tag": "{{DATE}}", "value": "December 15, 2024" },
    { "tag": "{{COMPANY}}", "value": "Acme Corporation", "sheets": ["Summary", "Cover"] }
  ],
  "cellUpdates": [
    { "sheet": "Data", "address": "B5", "value": 1250000 },
    { "sheet": "Data", "address": "B6", "value": 750000 }
  ]
}
```

#### Step 1c: Generate Output

```bash
deno run --allow-read --allow-write scripts/generate-from-template.ts \
  financial-template.xlsx replacements.json output.xlsx
```

### Mode 2: From-Scratch Generation

#### Step 2a: Create Specification

Create `spec.json`:
```json
{
  "title": "Sales Report",
  "author": "Finance Team",
  "sheets": [
    {
      "name": "Sales Data",
      "data": [
        ["Product", "Q1", "Q2", "Q3", "Q4", "Total"],
        ["Widget A", 10000, 12000, 15000, 18000, null],
        ["Widget B", 8000, 9000, 11000, 13000, null],
        ["Widget C", 5000, 6000, 7000, 8000, null]
      ],
      "cells": [
        { "address": "F2", "formula": "SUM(B2:E2)" },
        { "address": "F3", "formula": "SUM(B3:E3)" },
        { "address": "F4", "formula": "SUM(B4:E4)" }
      ],
      "columns": [
        { "col": "A", "width": 15 },
        { "col": "B", "width": 10 },
        { "col": "C", "width": 10 },
        { "col": "D", "width": 10 },
        { "col": "E", "width": 10 },
        { "col": "F", "width": 12 }
      ],
      "freezePane": "A2",
      "autoFilter": "A1:F4"
    }
  ]
}
```

#### Step 2b: Generate Spreadsheet

```bash
deno run --allow-read --allow-write scripts/generate-scratch.ts spec.json output.xlsx
```

## Examples

### Example 1: Monthly Sales Report

**Scenario**: Generate a monthly sales report from template.

**Steps**:
```bash
# 1. Analyze template for replaceable content
deno run --allow-read scripts/analyze-template.ts sales-template.xlsx --pretty

# 2. Create replacements.json with monthly data
# 3. Generate report
deno run --allow-read --allow-write scripts/generate-from-template.ts \
  sales-template.xlsx replacements.json November-Sales.xlsx
```

### Example 2: Data Export with Formulas

**Scenario**: Create a spreadsheet with calculated totals.

**spec.json**:
```json
{
  "sheets": [{
    "name": "Expenses",
    "data": [
      ["Category", "January", "February", "March", "Total"],
      ["Office", 1500, 1600, 1400, null],
      ["Travel", 3000, 2500, 4000, null],
      ["Software", 500, 500, 500, null],
      ["Total", null, null, null, null]
    ],
    "cells": [
      { "address": "E2", "formula": "SUM(B2:D2)" },
      { "address": "E3", "formula": "SUM(B3:D3)" },
      { "address": "E4", "formula": "SUM(B4:D4)" },
      { "address": "B5", "formula": "SUM(B2:B4)" },
      { "address": "C5", "formula": "SUM(C2:C4)" },
      { "address": "D5", "formula": "SUM(D2:D4)" },
      { "address": "E5", "formula": "SUM(E2:E4)" }
    ]
  }]
}
```

### Example 3: Multi-Sheet Workbook

**Scenario**: Create a workbook with summary and detail sheets.

**spec.json**:
```json
{
  "title": "Q4 Report",
  "sheets": [
    {
      "name": "Summary",
      "data": [
        ["Department", "Budget", "Actual", "Variance"],
        ["Sales", 500000, 520000, null],
        ["Marketing", 200000, 195000, null]
      ],
      "cells": [
        { "address": "D2", "formula": "C2-B2" },
        { "address": "D3", "formula": "C3-B3" }
      ]
    },
    {
      "name": "Sales Detail",
      "data": [
        ["Month", "Revenue", "Cost", "Profit"],
        ["October", 180000, 120000, null],
        ["November", 170000, 115000, null],
        ["December", 170000, 110000, null]
      ],
      "cells": [
        { "address": "D2", "formula": "B2-C2" },
        { "address": "D3", "formula": "B3-C3" },
        { "address": "D4", "formula": "B4-C4" }
      ]
    }
  ]
}
```

## Script Reference

| Script | Purpose | Permissions |
|--------|---------|-------------|
| `analyze-template.ts` | Extract cells, formulas, placeholders from XLSX | `--allow-read` |
| `generate-from-template.ts` | Replace placeholders in templates | `--allow-read --allow-write` |
| `generate-scratch.ts` | Create XLSX from JSON specification | `--allow-read --allow-write` |

## Specification Reference

### Sheet Options

| Property | Type | Description |
|----------|------|-------------|
| `name` | string | Sheet name |
| `data` | array | 2D array of cell values starting at A1 |
| `cells` | array | Individual cell specifications |
| `rows` | array | Row-based data specifications |
| `columns` | array | Column width and visibility settings |
| `merges` | array | Merged cell ranges |
| `freezePane` | string | Freeze panes at this cell (e.g., "A2") |
| `autoFilter` | string | Auto-filter range (e.g., "A1:F10") |

### Cell Options

| Property | Type | Description |
|----------|------|-------------|
| `address` | string | Cell address (e.g., "A1", "B2") |
| `value` | mixed | Cell value (string, number, boolean, null) |
| `formula` | string | Formula without = sign |
| `format` | string | Number format (e.g., "#,##0.00") |
| `type` | string | Force type: "string", "number", "boolean", "date" |

### Column Options

| Property | Type | Description |
|----------|------|-------------|
| `col` | string | Column letter (e.g., "A", "B", "AA") |
| `width` | number | Column width in characters |
| `hidden` | boolean | Hide column |

### Template Replacement Options

| Property | Type | Description |
|----------|------|-------------|
| `tag` | string | Placeholder to find (e.g., "{{TITLE}}") |
| `value` | mixed | Replacement value |
| `sheets` | array | Limit to specific sheets |
| `range` | string | Limit to cell range (e.g., "A1:D10") |

## Common Issues and Solutions

### Issue: Placeholders not being replaced

**Symptoms**: Output XLSX still contains `{{PLACEHOLDER}}` tags.

**Solution**:
1. Run `analyze-template.ts` to verify exact tag text and location
2. Check that placeholder is in a string cell, not a formula
3. Verify sheet filter in replacement spec

### Issue: Formulas showing as text

**Symptoms**: Formulas display as text instead of calculating.

**Solution**:
- Ensure formula doesn't start with "=" in spec (it's added automatically)
- Check cell type is not forced to "string"

### Issue: Numbers formatted as text

**Symptoms**: Numbers have green triangle indicating text storage.

**Solution**:
- Use numeric values in spec, not quoted strings
- For template replacement, if entire cell is placeholder and replacement is number, it converts automatically

### Issue: Column widths not applied

**Symptoms**: Columns have default width despite specification.

**Solution**:
- Ensure column letters are uppercase
- Verify column spec is in array format

## Limitations

- **XLSX only**: Does not support legacy .xls or .xlsb formats
- **No macros**: Cannot create or preserve VBA macros
- **Limited charting**: No native chart creation support
- **No pivot tables**: Cannot create pivot tables programmatically
- **Basic styling**: Limited cell formatting options
- **No conditional formatting**: Cannot set conditional format rules
- **Formula recalc**: Formulas are stored but not recalculated (Excel recalculates on open)

## Related Skills

- **pptx-generator**: For creating PowerPoint presentations
- **docx-generator**: For creating Word documents
- **csv-processor**: For simpler CSV data processing


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/analyze-template.ts

```typescript
#!/usr/bin/env -S deno run --allow-read

/**
 * analyze-template.ts - Extract structure and content from XLSX files
 *
 * Extracts sheets, cells, formulas, named ranges, and placeholders from Excel
 * spreadsheets for template analysis and content replacement planning.
 *
 * Usage:
 *   deno run --allow-read scripts/analyze-template.ts <input.xlsx> [options]
 *
 * Options:
 *   -h, --help       Show help
 *   -v, --verbose    Enable verbose output
 *   --pretty         Pretty-print JSON output
 *   --sheet <name>   Analyze only specific sheet
 *
 * Permissions:
 *   --allow-read: Read XLSX file
 */

import { parseArgs } from "jsr:@std/[email protected]/parse-args";
import { basename } from "jsr:@std/[email protected]";
// deno-lint-ignore no-explicit-any
import * as XLSX from "npm:[email protected]" as any;

// === Types ===

export interface CellInfo {
  address: string;
  row: number;
  col: number;
  value: string | number | boolean | null;
  formula?: string;
  type: "string" | "number" | "boolean" | "date" | "error" | "empty";
  format?: string;
}

export interface PlaceholderInfo {
  tag: string;
  location: string;
  sheet: string;
  address: string;
}

export interface SheetInfo {
  name: string;
  rowCount: number;
  colCount: number;
  usedRange: string;
  cells: CellInfo[];
  mergedCells: string[];
}

export interface NamedRange {
  name: string;
  ref: string;
  scope?: string;
}

export interface SpreadsheetInventory {
  filename: string;
  sheetCount: number;
  sheets: SheetInfo[];
  namedRanges: NamedRange[];
  placeholders: PlaceholderInfo[];
  hasFormulas: boolean;
  hasMacros: boolean;
}

interface ParsedArgs {
  help: boolean;
  verbose: boolean;
  pretty: boolean;
  sheet?: string;
  _: (string | number)[];
}

// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "analyze-template";

// Placeholder patterns: {{PLACEHOLDER}} or ${placeholder}
const PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}|\$\{([^}]+)\}/g;

// === Help Text ===
function printHelp(): void {
  console.log(`
${SCRIPT_NAME} v${VERSION} - Extract structure from XLSX templates

Usage:
  deno run --allow-read scripts/${SCRIPT_NAME}.ts <input.xlsx> [options]

Arguments:
  <input.xlsx>     Path to the Excel spreadsheet to analyze

Options:
  -h, --help       Show this help message
  -v, --verbose    Enable verbose output (to stderr)
  --pretty         Pretty-print JSON output (default: compact)
  --sheet <name>   Analyze only specific sheet

Examples:
  # Analyze spreadsheet
  deno run --allow-read scripts/${SCRIPT_NAME}.ts template.xlsx > inventory.json

  # With verbose output
  deno run --allow-read scripts/${SCRIPT_NAME}.ts template.xlsx -v --pretty

  # Analyze specific sheet
  deno run --allow-read scripts/${SCRIPT_NAME}.ts template.xlsx --sheet "Data"
`);
}

// === Utility Functions ===

function getCellType(cell: XLSX.CellObject | undefined): CellInfo["type"] {
  if (!cell) return "empty";
  switch (cell.t) {
    case "s": return "string";
    case "n": return "number";
    case "b": return "boolean";
    case "d": return "date";
    case "e": return "error";
    default: return "empty";
  }
}

function getCellValue(cell: XLSX.CellObject | undefined): string | number | boolean | null {
  if (!cell) return null;
  if (cell.v === undefined || cell.v === null) return null;
  return cell.v;
}

function decodeRange(range: string): { startRow: number; startCol: number; endRow: number; endCol: number } {
  const decoded = XLSX.utils.decode_range(range);
  return {
    startRow: decoded.s.r,
    startCol: decoded.s.c,
    endRow: decoded.e.r,
    endCol: decoded.e.c,
  };
}

function findPlaceholders(
  text: string,
  sheet: string,
  address: string
): PlaceholderInfo[] {
  const placeholders: PlaceholderInfo[] = [];
  let match;

  const regex = new RegExp(PLACEHOLDER_REGEX.source, "g");
  while ((match = regex.exec(text)) !== null) {
    placeholders.push({
      tag: match[0],
      location: `${sheet}!${address}`,
      sheet,
      address,
    });
  }

  return placeholders;
}

// === Core Logic ===

export async function analyzeSpreadsheet(
  xlsxPath: string,
  options: { verbose?: boolean; sheetFilter?: string } = {}
): Promise<SpreadsheetInventory> {
  const { verbose = false, sheetFilter } = options;

  // Read the XLSX file
  const data = await Deno.readFile(xlsxPath);
  const workbook = XLSX.read(data, { type: "array", cellFormula: true, cellStyles: true });

  const filename = basename(xlsxPath);

  if (verbose) {
    console.error(`Analyzing: ${filename}`);
    console.error(`Sheets: ${workbook.SheetNames.join(", ")}`);
  }

  const sheets: SheetInfo[] = [];
  const allPlaceholders: PlaceholderInfo[] = [];
  let hasFormulas = false;

  // Process each sheet
  for (const sheetName of workbook.SheetNames) {
    if (sheetFilter && sheetName !== sheetFilter) {
      continue;
    }

    const worksheet = workbook.Sheets[sheetName];
    if (!worksheet) continue;

    const ref = worksheet["!ref"];
    if (!ref) {
      // Empty sheet
      sheets.push({
        name: sheetName,
        rowCount: 0,
        colCount: 0,
        usedRange: "",
        cells: [],
        mergedCells: [],
      });
      continue;
    }

    const range = decodeRange(ref);
    const cells: CellInfo[] = [];

    // Iterate through all cells in range
    for (let row = range.startRow; row <= range.endRow; row++) {
      for (let col = range.startCol; col <= range.endCol; col++) {
        const address = XLSX.utils.encode_cell({ r: row, c: col });
        const cell = worksheet[address] as XLSX.CellObject | undefined;

        if (cell && cell.v !== undefined) {
          const cellInfo: CellInfo = {
            address,
            row: row + 1,  // 1-indexed for display
            col: col + 1,
            value: getCellValue(cell),
            type: getCellType(cell),
          };

          if (cell.f) {
            cellInfo.formula = cell.f;
            hasFormulas = true;
          }

          if (cell.z) {
            cellInfo.format = cell.z;
          }

          cells.push(cellInfo);

          // Check for placeholders in string values
          if (cellInfo.type === "string" && typeof cellInfo.value === "string") {
            const placeholders = findPlaceholders(cellInfo.value, sheetName, address);
            allPlaceholders.push(...placeholders);
          }
        }
      }
    }

    // Get merged cells
    const mergedCells: string[] = [];
    if (worksheet["!merges"]) {
      for (const merge of worksheet["!merges"]) {
        const startCell = XLSX.utils.encode_cell(merge.s);
        const endCell = XLSX.utils.encode_cell(merge.e);
        mergedCells.push(`${startCell}:${endCell}`);
      }
    }

    sheets.push({
      name: sheetName,
      rowCount: range.endRow - range.startRow + 1,
      colCount: range.endCol - range.startCol + 1,
      usedRange: ref,
      cells,
      mergedCells,
    });

    if (verbose) {
      console.error(`Sheet "${sheetName}": ${cells.length} cells, ${mergedCells.length} merged regions`);
    }
  }

  // Get named ranges
  const namedRanges: NamedRange[] = [];
  if (workbook.Workbook?.Names) {
    for (const name of workbook.Workbook.Names) {
      namedRanges.push({
        name: name.Name,
        ref: name.Ref,
        scope: name.Sheet !== undefined ? workbook.SheetNames[name.Sheet] : undefined,
      });
    }
  }

  // Check for macros
  const hasMacros = workbook.vbaraw !== undefined;

  if (verbose) {
    console.error(`Named ranges: ${namedRanges.length}`);
    console.error(`Placeholders found: ${allPlaceholders.length}`);
    console.error(`Has formulas: ${hasFormulas}`);
    console.error(`Has macros: ${hasMacros}`);
  }

  return {
    filename,
    sheetCount: sheets.length,
    sheets,
    namedRanges,
    placeholders: allPlaceholders,
    hasFormulas,
    hasMacros,
  };
}

// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
  const parsed = parseArgs(args, {
    boolean: ["help", "verbose", "pretty"],
    string: ["sheet"],
    alias: { help: "h", verbose: "v" },
    default: { verbose: false, pretty: false },
  }) as ParsedArgs;

  if (parsed.help) {
    printHelp();
    Deno.exit(0);
  }

  const positionalArgs = parsed._.map(String);

  if (positionalArgs.length === 0) {
    console.error("Error: No input file provided\n");
    printHelp();
    Deno.exit(1);
  }

  const inputPath = positionalArgs[0];

  try {
    const inventory = await analyzeSpreadsheet(inputPath, {
      verbose: parsed.verbose,
      sheetFilter: parsed.sheet,
    });

    const output = parsed.pretty
      ? JSON.stringify(inventory, null, 2)
      : JSON.stringify(inventory);
    console.log(output);
  } catch (error) {
    console.error(
      "Error:",
      error instanceof Error ? error.message : String(error)
    );
    Deno.exit(1);
  }
}

// === Entry Point ===
if (import.meta.main) {
  main(Deno.args);
}

```

### scripts/generate-from-template.ts

```typescript
#!/usr/bin/env -S deno run --allow-read --allow-write

/**
 * generate-from-template.ts - Generate XLSX from existing templates
 *
 * Modifies existing Excel templates using placeholder replacement.
 * Finds and replaces tagged content (e.g., {{TITLE}}, ${date}) in cells
 * across all sheets. Preserves formulas, formatting, and structure.
 *
 * Usage:
 *   deno run --allow-read --allow-write scripts/generate-from-template.ts <template.xlsx> <spec.json> <output.xlsx>
 *
 * Options:
 *   -h, --help       Show help
 *   -v, --verbose    Enable verbose output
 *
 * Permissions:
 *   --allow-read: Read template and specification files
 *   --allow-write: Write output XLSX file
 */

import { parseArgs } from "jsr:@std/[email protected]/parse-args";
import { basename } from "jsr:@std/[email protected]";
// deno-lint-ignore no-explicit-any
import * as XLSX from "npm:[email protected]" as any;

// === Types ===

export interface TextReplacement {
  /** The tag to find and replace (e.g., "{{TITLE}}" or "${date}") */
  tag: string;
  /** The replacement value */
  value: string | number | boolean;
  /** Optional: only apply to specific sheets */
  sheets?: string[];
  /** Optional: only apply to specific cell range (e.g., "A1:D10") */
  range?: string;
}

export interface CellUpdate {
  /** Sheet name */
  sheet: string;
  /** Cell address (e.g., "A1") */
  address: string;
  /** New value */
  value: string | number | boolean | null;
  /** Optional formula (without = sign) */
  formula?: string;
}

export interface TemplateSpec {
  /** Text replacements to apply */
  textReplacements?: TextReplacement[];
  /** Direct cell updates */
  cellUpdates?: CellUpdate[];
  /** Sheets to include (all if omitted) */
  includeSheets?: string[];
  /** Sheets to exclude */
  excludeSheets?: string[];
}

interface ParsedArgs {
  help: boolean;
  verbose: boolean;
  _: (string | number)[];
}

// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-from-template";

// === Help Text ===
function printHelp(): void {
  console.log(`
${SCRIPT_NAME} v${VERSION} - Generate XLSX from existing templates

Usage:
  deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts <template.xlsx> <spec.json> <output.xlsx>

Arguments:
  <template.xlsx>  Path to the template Excel file
  <spec.json>      Path to JSON specification for replacements
  <output.xlsx>    Path for output Excel file

Options:
  -h, --help       Show this help message
  -v, --verbose    Enable verbose output

Specification Format:
  {
    "textReplacements": [
      { "tag": "{{TITLE}}", "value": "Q4 2024 Report" },
      { "tag": "{{DATE}}", "value": "2024-12-15" },
      { "tag": "\${author}", "value": "Finance Team", "sheets": ["Summary"] }
    ],
    "cellUpdates": [
      { "sheet": "Data", "address": "B5", "value": 1250000 },
      { "sheet": "Data", "address": "C5", "formula": "B5*1.1" }
    ]
  }

Examples:
  # Replace text in template
  deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
    template.xlsx replacements.json output.xlsx

  # With verbose output
  deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
    template.xlsx replacements.json output.xlsx -v
`);
}

// === Utility Functions ===

function escapeRegExp(string: string): string {
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function getCellType(value: unknown): string {
  if (typeof value === "string") return "s";
  if (typeof value === "number") return "n";
  if (typeof value === "boolean") return "b";
  if (value instanceof Date) return "d";
  return "s";
}

function isInRange(address: string, range: string): boolean {
  const decoded = XLSX.utils.decode_range(range);
  const cell = XLSX.utils.decode_cell(address);
  return (
    cell.r >= decoded.s.r &&
    cell.r <= decoded.e.r &&
    cell.c >= decoded.s.c &&
    cell.c <= decoded.e.c
  );
}

// === Core Logic ===

interface ReplacementStats {
  replacements: number;
  updates: number;
}

export async function generateFromTemplate(
  templatePath: string,
  spec: TemplateSpec,
  outputPath: string,
  options: { verbose?: boolean } = {}
): Promise<ReplacementStats> {
  const { verbose = false } = options;

  // Read template
  const templateData = await Deno.readFile(templatePath);
  const workbook = XLSX.read(templateData, {
    type: "array",
    cellFormula: true,
    cellStyles: true,
    cellNF: true,
  });

  if (verbose) {
    console.error(`Loaded template: ${basename(templatePath)}`);
    console.error(`Sheets: ${workbook.SheetNames.join(", ")}`);
  }

  const textReplacements = spec.textReplacements || [];
  const cellUpdates = spec.cellUpdates || [];

  if (verbose) {
    console.error(`Text replacements: ${textReplacements.length}`);
    console.error(`Cell updates: ${cellUpdates.length}`);
  }

  let totalReplacements = 0;
  let totalUpdates = 0;

  // Determine which sheets to process
  let sheetsToProcess = [...workbook.SheetNames];
  if (spec.includeSheets && spec.includeSheets.length > 0) {
    sheetsToProcess = sheetsToProcess.filter((s) =>
      spec.includeSheets!.includes(s)
    );
  }
  if (spec.excludeSheets && spec.excludeSheets.length > 0) {
    sheetsToProcess = sheetsToProcess.filter(
      (s) => !spec.excludeSheets!.includes(s)
    );
  }

  // Process each sheet
  for (const sheetName of sheetsToProcess) {
    const worksheet = workbook.Sheets[sheetName];
    if (!worksheet) continue;

    const ref = worksheet["!ref"];
    if (!ref) continue;

    if (verbose) {
      console.error(`Processing sheet: ${sheetName}`);
    }

    let sheetReplacements = 0;

    // Process text replacements
    for (const key of Object.keys(worksheet)) {
      if (key.startsWith("!")) continue;

      const cell = worksheet[key];
      if (!cell || cell.t !== "s" || typeof cell.v !== "string") continue;

      let text = cell.v;
      const originalText = text;

      for (const replacement of textReplacements) {
        // Check sheet filter
        if (replacement.sheets && !replacement.sheets.includes(sheetName)) {
          continue;
        }

        // Check range filter
        if (replacement.range && !isInRange(key, replacement.range)) {
          continue;
        }

        // Perform replacement
        if (text.includes(replacement.tag)) {
          const regex = new RegExp(escapeRegExp(replacement.tag), "g");
          const matches = text.match(regex);
          if (matches) {
            sheetReplacements += matches.length;
          }
          text = text.replace(regex, String(replacement.value));
        }
      }

      if (text !== originalText) {
        // If the entire cell was a placeholder and replacement is a number, use number type
        if (
          originalText.trim() === textReplacements.find((r) => originalText.includes(r.tag))?.tag &&
          typeof textReplacements.find((r) => originalText.includes(r.tag))?.value === "number"
        ) {
          cell.t = "n";
          cell.v = Number(text);
        } else {
          cell.v = text;
        }
        // Clear any cached value
        delete cell.w;
      }
    }

    if (verbose && sheetReplacements > 0) {
      console.error(`  ${sheetReplacements} replacements`);
    }
    totalReplacements += sheetReplacements;
  }

  // Process direct cell updates
  for (const update of cellUpdates) {
    const worksheet = workbook.Sheets[update.sheet];
    if (!worksheet) {
      if (verbose) {
        console.error(`Warning: Sheet "${update.sheet}" not found`);
      }
      continue;
    }

    if (update.formula) {
      worksheet[update.address] = {
        t: "n",
        f: update.formula,
      };
    } else if (update.value !== null && update.value !== undefined) {
      worksheet[update.address] = {
        t: getCellType(update.value),
        v: update.value,
      };
    } else {
      // Clear the cell
      delete worksheet[update.address];
    }

    totalUpdates++;
  }

  if (verbose) {
    console.error(`Total replacements: ${totalReplacements}`);
    console.error(`Total updates: ${totalUpdates}`);
  }

  // Write output file
  const xlsxData = XLSX.write(workbook, {
    type: "buffer",
    bookType: "xlsx",
    compression: true,
  });

  await Deno.writeFile(outputPath, new Uint8Array(xlsxData));

  if (verbose) {
    console.error(`Wrote ${outputPath}`);
  }

  return { replacements: totalReplacements, updates: totalUpdates };
}

// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
  const parsed = parseArgs(args, {
    boolean: ["help", "verbose"],
    alias: { help: "h", verbose: "v" },
    default: { verbose: false },
  }) as ParsedArgs;

  if (parsed.help) {
    printHelp();
    Deno.exit(0);
  }

  const positionalArgs = parsed._.map(String);

  if (positionalArgs.length < 3) {
    console.error(
      "Error: template.xlsx, spec.json, and output.xlsx are required\n"
    );
    printHelp();
    Deno.exit(1);
  }

  const templatePath = positionalArgs[0];
  const specPath = positionalArgs[1];
  const outputPath = positionalArgs[2];

  try {
    // Read specification
    const specText = await Deno.readTextFile(specPath);
    const spec = JSON.parse(specText) as TemplateSpec;

    await generateFromTemplate(templatePath, spec, outputPath, {
      verbose: parsed.verbose,
    });

    console.log(`Created: ${outputPath}`);
  } catch (error) {
    console.error(
      "Error:",
      error instanceof Error ? error.message : String(error)
    );
    Deno.exit(1);
  }
}

// === Entry Point ===
if (import.meta.main) {
  main(Deno.args);
}

```

### scripts/generate-scratch.ts

```typescript
#!/usr/bin/env -S deno run --allow-read --allow-write

/**
 * generate-scratch.ts - Create XLSX from scratch using JSON specification
 *
 * Creates Excel spreadsheets programmatically from a JSON specification
 * using the xlsx library. Supports sheets, cells, formulas, formatting, and charts.
 *
 * Usage:
 *   deno run --allow-read --allow-write scripts/generate-scratch.ts <spec.json> <output.xlsx>
 *
 * Options:
 *   -h, --help       Show help
 *   -v, --verbose    Enable verbose output
 *
 * Permissions:
 *   --allow-read: Read specification file
 *   --allow-write: Write output XLSX file
 */

import { parseArgs } from "jsr:@std/[email protected]/parse-args";
// deno-lint-ignore no-explicit-any
import * as XLSX from "npm:[email protected]" as any;

// === Types ===

export interface CellSpec {
  /** Cell address (e.g., "A1", "B2") */
  address: string;
  /** Cell value */
  value: string | number | boolean | null;
  /** Formula (without = sign) */
  formula?: string;
  /** Number format (e.g., "0.00", "#,##0", "yyyy-mm-dd") */
  format?: string;
  /** Cell type override */
  type?: "string" | "number" | "boolean" | "date";
}

export interface RowSpec {
  /** Row number (1-indexed) */
  row: number;
  /** Cell values starting from column A */
  values: (string | number | boolean | null)[];
  /** Optional formulas for cells (same index as values) */
  formulas?: (string | undefined)[];
}

export interface ColumnSpec {
  /** Column letter (e.g., "A", "B", "AA") */
  col: string;
  /** Column width in characters */
  width?: number;
  /** Column hidden */
  hidden?: boolean;
}

export interface MergeSpec {
  /** Start cell address */
  start: string;
  /** End cell address */
  end: string;
}

export interface SheetSpec {
  /** Sheet name */
  name: string;
  /** Individual cell specifications */
  cells?: CellSpec[];
  /** Row-based data (alternative to cells) */
  rows?: RowSpec[];
  /** 2D array data starting at A1 (alternative to cells/rows) */
  data?: (string | number | boolean | null)[][];
  /** Column specifications */
  columns?: ColumnSpec[];
  /** Merged cell ranges */
  merges?: MergeSpec[];
  /** Freeze panes at this cell (e.g., "A2" freezes first row) */
  freezePane?: string;
  /** Auto-filter range (e.g., "A1:D10") */
  autoFilter?: string;
  /** Tab color (hex, no #) */
  tabColor?: string;
}

export interface SpreadsheetSpec {
  /** Workbook title (metadata) */
  title?: string;
  /** Author (metadata) */
  author?: string;
  /** Company (metadata) */
  company?: string;
  /** Sheets to create */
  sheets: SheetSpec[];
}

interface ParsedArgs {
  help: boolean;
  verbose: boolean;
  _: (string | number)[];
}

// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-scratch";

// === Help Text ===
function printHelp(): void {
  console.log(`
${SCRIPT_NAME} v${VERSION} - Create XLSX from scratch using JSON specification

Usage:
  deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts <spec.json> <output.xlsx>

Arguments:
  <spec.json>      Path to JSON specification file
  <output.xlsx>    Path for output Excel file

Options:
  -h, --help       Show this help message
  -v, --verbose    Enable verbose output

Specification Format:
  {
    "title": "Sales Report",
    "sheets": [
      {
        "name": "Data",
        "data": [
          ["Product", "Price", "Quantity", "Total"],
          ["Widget A", 10.99, 100, null],
          ["Widget B", 24.99, 50, null]
        ],
        "cells": [
          { "address": "D2", "formula": "B2*C2" },
          { "address": "D3", "formula": "B3*C3" }
        ],
        "columns": [
          { "col": "A", "width": 15 },
          { "col": "B", "width": 10 }
        ]
      }
    ]
  }

Examples:
  # Generate spreadsheet
  deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts spec.json output.xlsx

  # With verbose output
  deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts spec.json output.xlsx -v
`);
}

// === Utility Functions ===

function colNameToIndex(colName: string): number {
  let index = 0;
  for (let i = 0; i < colName.length; i++) {
    index = index * 26 + (colName.charCodeAt(i) - 64);
  }
  return index - 1;
}

function getCellType(value: unknown, specType?: string): string {
  if (specType) {
    switch (specType) {
      case "string": return "s";
      case "number": return "n";
      case "boolean": return "b";
      case "date": return "d";
    }
  }
  if (typeof value === "string") return "s";
  if (typeof value === "number") return "n";
  if (typeof value === "boolean") return "b";
  if (value instanceof Date) return "d";
  return "s";
}

// === Core Logic ===

export async function generateFromSpec(
  spec: SpreadsheetSpec,
  outputPath: string,
  options: { verbose?: boolean } = {}
): Promise<void> {
  const { verbose = false } = options;

  // Create workbook
  const workbook = XLSX.utils.book_new();

  // Set workbook properties
  if (spec.title || spec.author || spec.company) {
    workbook.Props = {
      Title: spec.title,
      Author: spec.author,
      Company: spec.company,
    };
  }

  if (verbose) {
    console.error(`Creating workbook with ${spec.sheets.length} sheet(s)`);
  }

  // Process each sheet
  for (const sheetSpec of spec.sheets) {
    if (verbose) {
      console.error(`Processing sheet: ${sheetSpec.name}`);
    }

    // deno-lint-ignore no-explicit-any
    let worksheet: any;

    // Create worksheet from data array
    if (sheetSpec.data && sheetSpec.data.length > 0) {
      worksheet = XLSX.utils.aoa_to_sheet(sheetSpec.data);
      if (verbose) {
        console.error(`  Created from ${sheetSpec.data.length} rows of data`);
      }
    } else {
      worksheet = {};
    }

    // Process row specifications
    if (sheetSpec.rows) {
      for (const rowSpec of sheetSpec.rows) {
        for (let colIdx = 0; colIdx < rowSpec.values.length; colIdx++) {
          const value = rowSpec.values[colIdx];
          const formula = rowSpec.formulas?.[colIdx];
          const address = XLSX.utils.encode_cell({ r: rowSpec.row - 1, c: colIdx });

          if (formula) {
            worksheet[address] = { t: "n", f: formula };
          } else if (value !== null && value !== undefined) {
            worksheet[address] = {
              t: getCellType(value),
              v: value,
            };
          }
        }
      }
      if (verbose) {
        console.error(`  Processed ${sheetSpec.rows.length} row specs`);
      }
    }

    // Process individual cell specifications
    if (sheetSpec.cells) {
      for (const cellSpec of sheetSpec.cells) {
        // deno-lint-ignore no-explicit-any
        const cell: any = {};

        if (cellSpec.formula) {
          cell.t = "n";
          cell.f = cellSpec.formula;
        } else if (cellSpec.value !== null && cellSpec.value !== undefined) {
          cell.t = getCellType(cellSpec.value, cellSpec.type);
          cell.v = cellSpec.value;
        }

        if (cellSpec.format) {
          cell.z = cellSpec.format;
        }

        worksheet[cellSpec.address] = cell;
      }
      if (verbose) {
        console.error(`  Applied ${sheetSpec.cells.length} cell specs`);
      }
    }

    // Calculate the used range
    const range = { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } };
    let hasData = false;

    for (const key of Object.keys(worksheet)) {
      if (key.startsWith("!")) continue;
      hasData = true;
      const cell = XLSX.utils.decode_cell(key);
      range.e.r = Math.max(range.e.r, cell.r);
      range.e.c = Math.max(range.e.c, cell.c);
    }

    if (hasData) {
      worksheet["!ref"] = XLSX.utils.encode_range(range);
    }

    // Apply column specifications
    if (sheetSpec.columns) {
      worksheet["!cols"] = [];
      for (const colSpec of sheetSpec.columns) {
        const colIdx = colNameToIndex(colSpec.col);
        while (worksheet["!cols"].length <= colIdx) {
          worksheet["!cols"].push({});
        }
        worksheet["!cols"][colIdx] = {
          wch: colSpec.width,
          hidden: colSpec.hidden,
        };
      }
      if (verbose) {
        console.error(`  Applied ${sheetSpec.columns.length} column specs`);
      }
    }

    // Apply merged cells
    if (sheetSpec.merges && sheetSpec.merges.length > 0) {
      worksheet["!merges"] = sheetSpec.merges.map((merge) => ({
        s: XLSX.utils.decode_cell(merge.start),
        e: XLSX.utils.decode_cell(merge.end),
      }));
      if (verbose) {
        console.error(`  Applied ${sheetSpec.merges.length} merge specs`);
      }
    }

    // Apply freeze pane
    if (sheetSpec.freezePane) {
      const freezeCell = XLSX.utils.decode_cell(sheetSpec.freezePane);
      worksheet["!freeze"] = {
        xSplit: freezeCell.c,
        ySplit: freezeCell.r,
        topLeftCell: sheetSpec.freezePane,
      };
    }

    // Apply auto-filter
    if (sheetSpec.autoFilter) {
      worksheet["!autofilter"] = { ref: sheetSpec.autoFilter };
    }

    // Add sheet to workbook
    XLSX.utils.book_append_sheet(workbook, worksheet, sheetSpec.name);
  }

  // Write output file
  const xlsxData = XLSX.write(workbook, {
    type: "buffer",
    bookType: "xlsx",
    compression: true,
  });

  await Deno.writeFile(outputPath, new Uint8Array(xlsxData));

  if (verbose) {
    console.error(`Wrote ${outputPath}`);
  }
}

// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
  const parsed = parseArgs(args, {
    boolean: ["help", "verbose"],
    alias: { help: "h", verbose: "v" },
    default: { verbose: false },
  }) as ParsedArgs;

  if (parsed.help) {
    printHelp();
    Deno.exit(0);
  }

  const positionalArgs = parsed._.map(String);

  if (positionalArgs.length < 2) {
    console.error("Error: Both spec.json and output.xlsx are required\n");
    printHelp();
    Deno.exit(1);
  }

  const specPath = positionalArgs[0];
  const outputPath = positionalArgs[1];

  try {
    const specText = await Deno.readTextFile(specPath);
    const spec = JSON.parse(specText) as SpreadsheetSpec;

    await generateFromSpec(spec, outputPath, {
      verbose: parsed.verbose,
    });

    console.log(`Created: ${outputPath}`);
  } catch (error) {
    console.error(
      "Error:",
      error instanceof Error ? error.message : String(error)
    );
    Deno.exit(1);
  }
}

// === Entry Point ===
if (import.meta.main) {
  main(Deno.args);
}

```