// mime-todo/lib/file.ts import * as fs from "fs" import { fileURLToPath } from "url"; import { simpleParser } from "mailparser" import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { parseIssue, Issue } from "./issue" import { parseSprints, Sprint } from "./sprint" import * as schema from "./file.schema.json" import Ajv from "ajv" const ajv = new Ajv({ allErrors: true }) const validateFile = ajv.compile(schema) export interface TodoFile { sprints: Sprint[] issues: Issue[] } export async function parseMime(mimeText: string) { return await simpleParser(mimeText) } export function preprocessTODO(raw: string): string { const boundary = "ISSUE" const rawParts = raw .split(`--${boundary}`) .map(p => p.trim()) .filter(Boolean) interface Part { type: string body: string } const parts: Part[] = [] for (const rawPart of rawParts) { const lines = rawPart.split(/\r?\n/) const typeLine = lines[0] if (!typeLine?.toLowerCase().startsWith("content-type:")) { throw new Error(`Part missing Content-Type header:\n${rawPart}`) } const type = typeLine.slice("Content-Type:".length).trim() const body = lines.slice(1).join("\n").trim() parts.push({ type, body }) } // Validate sprint parts const sprintParts = parts.filter(p => p.type === "application/sprints") if (sprintParts.length > 1) { throw new Error("Multiple application/sprints parts found") } const sprintsPart = sprintParts[0] ?? null // Preserve unknown types, but reorder sprints first const orderedParts: Part[] = [] if (sprintsPart) orderedParts.push(sprintsPart) for (const part of parts) { if (part !== sprintsPart) orderedParts.push(part) } // MIME envelope const out: string[] = [] out.push(`MIME-Version: 1.0`) out.push(`Content-Type: multipart/mixed; boundary="${boundary}"`) out.push("") for (const part of orderedParts) { out.push(`--${boundary}`) out.push(`Content-Type: ${part.type}`) out.push("") out.push(part.body) out.push("") } out.push(`--${boundary}--`) out.push("") return out.join("\n") } export async function parseTodoFile(path = "TODO"): Promise { const raw = fs.readFileSync(path, "utf-8") const mimeWrapped = preprocessTODO(raw) const parsed = await parseMime(mimeWrapped) const sprints: Sprint[] = [] const issues: Issue[] = [] const parts = (parsed as any).attachments ?? [] for (const part of parts) { const contentType = String(part.contentType || "").toLowerCase() const body = part.content?.toString("utf-8") ?? "" if (contentType.startsWith("application/sprints")) { // Debug: Log what we're parsing // console.log("Parsing sprints:", body); sprints.push(...parseSprints(body)) } else if (contentType.startsWith("application/issue")) { issues.push(parseIssue(body)) } } const file = {sprints: sprints, issues: issues }; if (!validateFile(file)) { throw new Error( "Sprint schema validation failed: " + JSON.stringify(validateFile.errors, null, 2) ) } return file; } function index(yargs) { console.log(yargs.argv); } function _index(yargs) { return yargs.command('index', 'welcome ter yargs!', index) } if (process.argv[1] === fileURLToPath(import.meta.url)) { let argv = yargs() .usage('$0 [args]') .help() let commands = [ _index ] commands.forEach((method) => { method(argv) }); yargs.parse(hideBin(process.argv)) }