mime-todo-spec/scripts/ts-mime-todo/lib/file.ts
Tiara Rodney 76266cedc6
init
2026-02-10 18:59:59 +01:00

156 lines
3.5 KiB
TypeScript

// 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<TodoFile> {
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 <cmd> [args]')
.help()
let commands = [
_index
]
commands.forEach((method) => {
method(argv)
});
yargs.parse(hideBin(process.argv))
}