|
| 1 | +#!/usr/bin/env bun |
| 2 | +import { readFileSync } from 'node:fs' |
| 3 | +import { readFile } from 'node:fs/promises' |
| 4 | +import { resolve } from 'node:path' |
| 5 | +import process from 'node:process' |
| 6 | +import { CAC } from 'cac' |
| 7 | +import { type Content, createSpreadsheet, csvToContent } from '../src/index' |
| 8 | + |
| 9 | +// Use sync version for version to avoid race conditions |
| 10 | +const version = process.env.NODE_ENV === 'test' |
| 11 | + ? '0.0.0' |
| 12 | + : JSON.parse( |
| 13 | + readFileSync(resolve(__dirname, '../package.json'), 'utf-8'), |
| 14 | + ).version |
| 15 | + |
| 16 | +interface CLIOptions { |
| 17 | + type?: 'csv' | 'excel' |
| 18 | + output?: string |
| 19 | + pretty?: boolean |
| 20 | +} |
| 21 | + |
| 22 | +const cli = new CAC('spreadsheets') |
| 23 | + |
| 24 | +function logMessage(message: string) { |
| 25 | + if (process.env.NODE_ENV === 'test') { |
| 26 | + process.stderr.write(`${message}\n`) |
| 27 | + } |
| 28 | + else { |
| 29 | + console.log(message) |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +cli |
| 34 | + .command('create <input>', 'Create a spreadsheet from JSON input file') |
| 35 | + .option('-t, --type <type>', 'Output type (csv or excel)', { default: 'csv' }) |
| 36 | + .option('-o, --output <path>', 'Output file path') |
| 37 | + .action(async (input: string, options: CLIOptions) => { |
| 38 | + try { |
| 39 | + const inputPath = resolve(process.cwd(), input) |
| 40 | + const content = JSON.parse(await readFile(inputPath, 'utf-8')) as Content |
| 41 | + |
| 42 | + const result = createSpreadsheet(content, { type: options.type }) |
| 43 | + |
| 44 | + if (options.output) { |
| 45 | + await result.store(options.output) |
| 46 | + logMessage(`Spreadsheet saved to ${options.output}`) |
| 47 | + } |
| 48 | + else { |
| 49 | + const output = result.getContent() |
| 50 | + if (typeof output === 'string') { |
| 51 | + process.stdout.write(output) |
| 52 | + } |
| 53 | + else { |
| 54 | + process.stdout.write(output) |
| 55 | + } |
| 56 | + } |
| 57 | + } |
| 58 | + catch (error) { |
| 59 | + logMessage(`Failed to create spreadsheet: ${(error as Error).message}`) |
| 60 | + process.exit(1) |
| 61 | + } |
| 62 | + }) |
| 63 | + |
| 64 | +cli |
| 65 | + .command('convert <input> <output>', 'Convert between spreadsheet formats') |
| 66 | + .action(async (input: string, output: string) => { |
| 67 | + try { |
| 68 | + const inputExt = input.slice(input.lastIndexOf('.')) as '.csv' | '.xlsx' |
| 69 | + const outputExt = output.slice(output.lastIndexOf('.')) as '.csv' | '.xlsx' |
| 70 | + |
| 71 | + if (inputExt === outputExt) { |
| 72 | + logMessage('Input and output formats are the same') |
| 73 | + return |
| 74 | + } |
| 75 | + |
| 76 | + // Handle CSV input |
| 77 | + let content: Content |
| 78 | + if (inputExt === '.csv') { |
| 79 | + content = await csvToContent(input) |
| 80 | + } |
| 81 | + else { |
| 82 | + // Handle JSON input |
| 83 | + content = JSON.parse(await readFile(input, 'utf-8')) as Content |
| 84 | + } |
| 85 | + |
| 86 | + const outputType = outputExt === '.csv' ? 'csv' : 'excel' |
| 87 | + const result = createSpreadsheet(content, { type: outputType }) |
| 88 | + await result.store(output) |
| 89 | + |
| 90 | + logMessage(`Converted ${input} to ${output}`) |
| 91 | + } |
| 92 | + catch (error) { |
| 93 | + logMessage(`Failed to convert spreadsheet: ${(error as Error).message}`) |
| 94 | + process.exit(1) |
| 95 | + } |
| 96 | + }) |
| 97 | + |
| 98 | +cli |
| 99 | + .command('validate <input>', 'Validate JSON input format') |
| 100 | + .action(async (input: string) => { |
| 101 | + try { |
| 102 | + const content = JSON.parse(await readFile(input, 'utf-8')) |
| 103 | + |
| 104 | + if (!content.headings || !Array.isArray(content.headings)) { |
| 105 | + throw new Error('Missing or invalid headings array') |
| 106 | + } |
| 107 | + if (!content.data || !Array.isArray(content.data)) { |
| 108 | + throw new Error('Missing or invalid data array') |
| 109 | + } |
| 110 | + |
| 111 | + const invalidHeadings = content.headings.some((h: unknown) => typeof h !== 'string') |
| 112 | + if (invalidHeadings) { |
| 113 | + throw new Error('Headings must be strings') |
| 114 | + } |
| 115 | + |
| 116 | + const invalidData = content.data.some((row: unknown[]) => |
| 117 | + !Array.isArray(row) |
| 118 | + || row.some((cell: unknown) => typeof cell !== 'string' && typeof cell !== 'number'), |
| 119 | + ) |
| 120 | + if (invalidData) { |
| 121 | + throw new Error('Data must be an array of arrays containing only strings and numbers') |
| 122 | + } |
| 123 | + |
| 124 | + logMessage('Input JSON is valid') |
| 125 | + } |
| 126 | + catch (error) { |
| 127 | + logMessage(`Invalid input: ${(error as Error).message}`) |
| 128 | + process.exit(1) |
| 129 | + } |
| 130 | + }) |
| 131 | + |
| 132 | +cli.version(version) |
| 133 | +cli.help() |
| 134 | +cli.parse() |
0 commit comments