@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
JavaScript
// @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();