// Name: JSON Array to CSV or YAML // Description: Convert a JSON array from selection, clipboard, or file to CSV or YAML, save to Downloads, and copy to clipboard. // Author: dallascrilley // GitHub: dallascrilley import "@johnlindquist/kit" type SourceChoice = "selection" | "clipboard" | "file" type FormatChoice = "csv" | "yaml" const previewSnippet = (text: string, max = 300) => { const t = (text || "").trim() if (!t) return "(empty)" return t.length > max ? t.slice(0, max) + "…" : t } const safeParseJsonArray = (text: string): { ok: true; data: any[] } | { ok: false; error: string } => { try { const parsed = JSON.parse(text) if (!Array.isArray(parsed)) { return { ok: false, error: "Input JSON must be an array (e.g., [{...}, {...}])." } } return { ok: true, data: parsed } } catch (err: any) { return { ok: false, error: `Invalid JSON: ${err?.message || err}` } } } const toCsv = (rows: any[]): string => { if (!Array.isArray(rows) || rows.length === 0) return "" const keys = Object.keys(rows[0] ?? {}) const escapeValue = (v: any): string => { if (v === null || v === undefined) v = "" else if (typeof v === "object") v = JSON.stringify(v) else v = String(v) // RFC 4180: double quotes within fields, wrap fields containing special chars let s = String(v) const needsQuotes = /[",\r\n]/.test(s) if (s.includes(`"`)) s = s.replace(/"/g, `""`) return needsQuotes ? `"${s}"` : s } const header = keys.map(k => escapeValue(k)).join(",") const lines = rows.map(obj => keys.map(k => escapeValue(obj?.[k])).join(",")) return [header, ...lines].join("\r\n") } const toYaml = async (data: any[]): Promise<string> => { // Prefer the "yaml" package if available try { const YAML = await attemptImport("yaml") // @ts-ignore return YAML.stringify(data, { indent: 2 }) } catch { // Fallback to js-yaml try { const jsYaml = await attemptImport("js-yaml") // @ts-ignore return jsYaml.dump(data, { indent: 2, lineWidth: -1 }) } catch (err: any) { throw new Error("Missing YAML dependency. Please install 'yaml' or 'js-yaml' when prompted.") } } } const getDefaultSource = async (): Promise<SourceChoice> => { const selected = (await getSelectedText())?.trim() if (selected) return "selection" const clip = (await clipboard.readText())?.trim() if (clip) return "clipboard" return "file" } const getSourceText = async (source: SourceChoice): Promise<string | null> => { if (source === "selection") { return (await getSelectedText())?.trim() || "" } if (source === "clipboard") { return (await clipboard.readText())?.trim() || "" } // file const filePath = await selectFile("Select a JSON file") if (!filePath) return null try { const text = await readFile(filePath, "utf8") return String(text ?? "").trim() } catch (err: any) { await notify(`Failed to read file. ${err?.message || err}`) return null } } let data: any[] = [] let rawText = "" let canceled = false inputLoop: while (true) { const selectedText = (await getSelectedText())?.trim() || "" const clipboardText = (await clipboard.readText())?.trim() || "" const defaultSrc = await getDefaultSource() const choices = [ { name: "Use current text selection", value: "selection", description: previewSnippet(selectedText), disabled: !selectedText, }, { name: "Use clipboard content", value: "clipboard", description: previewSnippet(clipboardText), disabled: !clipboardText, }, { name: "Choose JSON file…", value: "file", description: "Pick a .json file from disk", }, ] let source: SourceChoice try { source = await arg<SourceChoice>( { placeholder: "Select input source for JSON array", enter: "Use Source", strict: true, onEscape: () => { canceled = true submit(null) }, hint: "Choose where to read the JSON array from", }, choices.map(c => ({ ...c, id: c.value === defaultSrc ? "default" : undefined, })) ) } catch { canceled = true break inputLoop } if (!source) { canceled = true break inputLoop } const text = await getSourceText(source) if (text == null) { // likely user canceled file picker const retry = await arg({ placeholder: "No input loaded. Try again?", hint: "Press enter to pick source again or Esc to cancel", enter: "Pick Source Again", }).catch(() => null) if (!retry) { canceled = true break inputLoop } continue } const parsed = safeParseJsonArray(text) if (!parsed.ok) { await notify(`Invalid JSON input. ${parsed.error}`) const tryAgain = await arg({ placeholder: "Invalid JSON. Try a different