UNPKG

@alexcatdad/calico-cli

Version:

Command-line tool for @alexcatdad/calico. Convert data between JSON, CSV, YAML, and Markdown formats from the terminal.

471 lines (467 loc) 13.2 kB
#!/usr/bin/env bun // @bun // ../core/dist/index.js function detectCircularReference(o, s = new WeakSet, p = "root") { if (o === null || typeof o !== "object") return; if (s.has(o)) throw new Error(`Circular reference detected at path '${p}' - object references itself`); s.add(o); for (const k in o) if (Object.prototype.hasOwnProperty.call(o, k)) detectCircularReference(o[k], s, `${p}.${k}`); s.delete(o); } var QUOTE_PATTERN = /"/g; function toCSV(data, options = {}) { if (!Array.isArray(data)) { throw new TypeError(`toCSV requires an array, received ${typeof data}`); } if (data.length === 0) { return ""; } detectCircularReference(data); const isObject = typeof data[0] === "object" && data[0] !== null; for (let i = 1;i < data.length; i++) { const itemIsObject = typeof data[i] === "object" && data[i] !== null; if (itemIsObject !== isObject) { throw new TypeError("CSV data must be array of objects or primitives, received mixed types"); } } const delimiter = options.delimiter || ","; const quoteStrings = options.quoteStrings ?? true; const includeHeaders = options.headers ?? true; const escapeField = (field) => { if (field === null || field === undefined) return ""; const str = String(field); if (quoteStrings || str.includes(delimiter) || str.includes('"') || str.includes(` `)) { return `"${str.replace(QUOTE_PATTERN, '""')}"`; } return str; }; if (!isObject) { const rows2 = data.map((item) => escapeField(item)); return rows2.join(` `); } const allKeys = new Set; for (let i = 0;i < data.length; i++) { const keys = Object.keys(data[i]); for (let j = 0;j < keys.length; j++) { allKeys.add(keys[j]); } } const headers = Array.from(allKeys); const rows = []; if (includeHeaders) { const headerRow = []; for (let i = 0;i < headers.length; i++) { headerRow.push(escapeField(headers[i])); } rows.push(headerRow.join(delimiter)); } for (let i = 0;i < data.length; i++) { const row = []; const item = data[i]; for (let j = 0;j < headers.length; j++) { row.push(escapeField(item[headers[j]])); } rows.push(row.join(delimiter)); } return rows.join(` `); } function fromCSV(input, options = {}) { if (typeof input !== "string") { throw new TypeError(`fromCSV input must be string, received ${typeof input}`); } if (!input.trim()) { return []; } const delimiter = options.delimiter || ","; const hasHeaders = options.headers ?? true; const rows = []; let currentRow = []; let currentField = ""; let inQuotes = false; for (let i = 0;i < input.length; i++) { const char = input[i]; const nextChar = input[i + 1]; if (inQuotes) { if (char === '"') { if (nextChar === '"') { currentField += '"'; i++; } else { inQuotes = false; } } else { currentField += char; } } else { if (char === '"') { inQuotes = true; } else if (char === delimiter) { currentRow.push(currentField); currentField = ""; } else if (char === ` ` || char === "\r" && nextChar === ` `) { currentRow.push(currentField); rows.push(currentRow); currentRow = []; currentField = ""; if (char === "\r") i++; } else if (char === "\r") { currentRow.push(currentField); rows.push(currentRow); currentRow = []; currentField = ""; } else { currentField += char; } } } if (currentField || currentRow.length > 0 || input.endsWith(delimiter)) { currentRow.push(currentField); rows.push(currentRow); } if (rows.length === 0) { return []; } if (!hasHeaders) { return rows; } const headers = rows[0]; const dataRows = rows.slice(1); return dataRows.map((row, rowIndex) => { const obj = {}; headers.forEach((header, index) => { obj[header] = row[index] !== undefined ? row[index] : ""; }); return obj; }); } function toJSON(data, pretty = true) { detectCircularReference(data); try { return JSON.stringify(data, null, pretty ? 2 : 0); } catch (error) { if (error instanceof Error) { throw new TypeError(`Input data for toJSON must be serializable: ${error.message}`); } throw error; } } function fromJSON(input) { if (typeof input !== "string") { throw new TypeError(`fromJSON input must be string, received ${typeof input}`); } try { return JSON.parse(input); } catch (error) { if (error instanceof SyntaxError) { throw new SyntaxError(`Invalid JSON: ${error.message}`); } throw error; } } function toMarkdown(data, options = {}) { detectCircularReference(data); const parts = []; if (options.title) { parts.push(`# ${options.title} `); } if (options.includeTableOfContents) { if (typeof data === "object" && data !== null && !Array.isArray(data)) { parts.push("## Table of Contents"); for (const key of Object.keys(data)) { parts.push(`- [${key}](#${key.toLowerCase()})`); } parts.push(""); } } if (Array.isArray(data)) { if (data.length > 0 && typeof data[0] === "object" && data[0] !== null) { const allKeys = new Set; for (const item of data) { if (typeof item === "object" && item !== null) { for (const k of Object.keys(item)) { allKeys.add(k); } } } const headers = Array.from(allKeys); parts.push(`| ${headers.join(" | ")} |`); parts.push(`| ${headers.map(() => "---").join(" | ")} |`); for (const item of data) { const row = headers.map((h) => { const val = item[h]; return val === undefined || val === null ? "" : String(val); }).join(" | "); parts.push(`| ${row} |`); } } else { for (const item of data) { parts.push(`- ${String(item)}`); } } } else if (typeof data === "object" && data !== null) { for (const [key, value] of Object.entries(data)) { parts.push(`- **${key}**: ${JSON.stringify(value)}`); } } else { parts.push(String(data)); } return parts.join(` `); } var SPECIAL_CHARS = /[:#\[\]{},"'\n\t]/; function toYAML(data, indent = 2) { detectCircularReference(data); if (indent < 0 || !Number.isInteger(indent)) throw new TypeError("YAML indentation must be positive integer"); return ser(data, indent, 0); } function ser(d, i, l) { const p = l > 0 ? " ".repeat(l * i) : ""; if (d === null) return "null"; if (typeof d === "undefined") return ""; if (typeof d !== "object") { if (typeof d === "string") { if (d === "" || SPECIAL_CHARS.test(d) || !Number.isNaN(Number(d)) || ["true", "false", "null"].includes(d)) { return `"${d.replace(/"/g, "\\\"")}"`; } return d; } return String(d); } if (Array.isArray(d)) { if (d.length === 0) return "[]"; const lines2 = []; for (let j = 0;j < d.length; j++) { const it = d[j]; const s = ser(it, i, l + 1); if (typeof it === "object" && it !== null && !Array.isArray(it)) { const ks = Object.keys(it); if (ks.length > 0) { const ln = s.split(` `); lines2.push(`${p}- ${ln[0]}${ln.length > 1 ? ` ${ln.slice(1).join(` `)}` : ""}`); continue; } } const trimIdx = s.search(/\S/); const trimmed = trimIdx > 0 ? s.slice(trimIdx) : s; lines2.push(`${p}- ${trimmed}`); } return lines2.join(` `); } const k = Object.keys(d); if (k.length === 0) return "{}"; const lines = []; for (let j = 0;j < k.length; j++) { const y = k[j]; const v = d[y]; const s = ser(v, i, l + 1); if (typeof v === "object" && v !== null) { const vk = Object.keys(v); if (vk.length > 0) { lines.push(`${p}${y}: ${s}`); continue; } } const trimIdx = s.search(/\S/); const trimmed = trimIdx > 0 ? s.slice(trimIdx) : s; lines.push(`${p}${y}: ${trimmed}`); } return lines.join(` `); } function fromYAML(input) { if (typeof input !== "string") throw new TypeError(`fromYAML input must be string, received ${typeof input}`); if (!input.trim()) return; const lines = input.split(` `); let cl = 0; function parse(mi) { let res = undefined; while (cl < lines.length) { const line = lines[cl]; if (!line.trim() || line.trim().startsWith("#")) { cl++; continue; } const ind = line.search(/\S/); if (ind < mi) break; const c = line.trim(); if (c.startsWith("- ")) { if (res === undefined) res = []; if (!Array.isArray(res)) throw new SyntaxError(`Invalid YAML at line ${cl + 1}: expected array item`); const v = c.substring(2).trim(); cl++; if (v) res.push(val(v)); else res.push(parse(ind + 1)); } else if (c.includes(":")) { const idx = c.indexOf(":"); const k = c.substring(0, idx).trim(); const v = c.substring(idx + 1).trim(); if (res === undefined) res = {}; if (Array.isArray(res)) throw new SyntaxError(`Invalid YAML at line ${cl + 1}: expected array item`); cl++; if (v) res[k] = val(v); else res[k] = parse(ind + 1); } else throw new SyntaxError(`Invalid YAML at line ${cl + 1}: unexpected token`); } return res; } function val(s) { if (s === "true") return true; if (s === "false") return false; if (s === "null") return null; if (!Number.isNaN(Number(s))) return Number(s); if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\"/g, '"'); return s; } try { const r = parse(0); return r === undefined ? {} : r; } catch (e) { if (e instanceof Error) throw e; throw new Error("Unknown YAML parse error"); } } class DataExporter { toJSON(data, pretty = true) { return toJSON(data, pretty); } toCSV(data, options = {}) { return toCSV(data, options); } toYAML(data, indent = 2) { return toYAML(data, indent); } toMarkdown(data, options = {}) { return toMarkdown(data, options); } fromJSON(input) { return fromJSON(input); } fromCSV(input, options = {}) { return fromCSV(input, options); } fromYAML(input) { return fromYAML(input); } } // src/index.ts import { readFileSync, writeFileSync } from "fs"; import { parseArgs } from "util"; async function main() { const { values, positionals } = parseArgs({ args: Bun.argv, options: { input: { type: "string", short: "i" }, output: { type: "string", short: "o" }, format: { type: "string", short: "f" }, pretty: { type: "boolean", short: "p" } }, strict: true, allowPositionals: true }); const inputFile = values.input || positionals[2]; const outputFile = values.output; const format = values.format; if (!inputFile) { console.error("Usage: calico -i <input-file> -o <output-file> -f <format>"); process.exit(1); } try { const inputContent = readFileSync(inputFile, "utf-8"); const exporter = new DataExporter; let data; if (inputFile.endsWith(".json")) { data = JSON.parse(inputContent); } else if (inputFile.endsWith(".csv")) { data = exporter.fromCSV(inputContent); } else if (inputFile.endsWith(".yaml") || inputFile.endsWith(".yml")) { data = exporter.fromYAML(inputContent); } else { try { data = JSON.parse(inputContent); } catch { console.error("Could not auto-detect input format. Please ensure input is valid JSON, CSV, or YAML."); process.exit(1); } } let outputContent = ""; const targetFormat = format || (outputFile ? outputFile.split(".").pop() : "json"); switch (targetFormat?.toLowerCase()) { case "json": outputContent = exporter.toJSON(data, values.pretty); break; case "csv": if (!Array.isArray(data)) { console.error("Error: CSV export requires array data"); process.exit(1); } outputContent = exporter.toCSV(data); break; case "yaml": case "yml": outputContent = exporter.toYAML(data); break; case "md": case "markdown": outputContent = exporter.toMarkdown(data); break; default: console.error(`Unsupported output format: ${targetFormat}`); process.exit(1); } if (outputFile) { writeFileSync(outputFile, outputContent); console.log(`Successfully exported to ${outputFile}`); } else { console.log(outputContent); } } catch (error) { if (error instanceof Error) { console.error(`Error: ${error.message}`); } else { console.error("An unknown error occurred"); } process.exit(1); } } main();