156 lines
3.5 KiB
TypeScript
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))
|
|
}
|