opencode-config/.opencode/draw-image/cli.ts
Sergey 56b67d2d5c
feat(draw-image): SVG template renderer for on-brand covers (#134)
* feat(draw-image): add SVG template render engine

* feat(draw-image): add opencode plugin and tool tests

* test(draw-image): add vitest unit integration e2e suite

* chore(draw-image): wire CI dependabot docs and ADR

* docs(handoff): set PR number

* docs(handoff): fix PR number placeholder in frontmatter

* fix(ci): use tempfile and trailing newline in draw-image tests

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-29 23:42:50 +03:00

84 lines
No EOL
2.6 KiB
TypeScript

import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { spawnSync } from "node:child_process"
import { loadBrand } from "./src/config.ts"
import { loadTemplate, buildSvg, computeHash } from "./src/render.ts"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
function parseArgs(argv: string[]): Record<string, string> {
const out: Record<string, string> = {}
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a.startsWith("--")) {
const key = a.slice(2)
const val = argv[i + 1] ?? ""
out[key] = val
i++
}
}
return out
}
function main() {
const argv = process.argv.slice(2)
if (argv.length < 2 || argv[0] !== "render") {
process.stderr.write('usage: node cli.ts render <template> --title "..." [--slots icon=mic] [--out path]\n')
process.exit(2)
}
const template = argv[1]
const opts = parseArgs(argv.slice(2))
const title = opts.title ?? ""
const subtitle = opts.subtitle
const out = opts.out ?? "./assets/cover.png"
const slots: Record<string, string> = {}
if (opts.slots) {
for (const pair of opts.slots.split(",")) {
const eq = pair.indexOf("=")
if (eq !== -1) slots[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim()
}
}
const brand = loadBrand(__dirname)
const templateSvg = loadTemplate(__dirname, template)
const hash = computeHash(brand, templateSvg, { template, title, subtitle, slots, out })
const outResolved = path.resolve(out)
const metaPath = outResolved.replace(/\.png$/, ".meta.json")
if (existsSync(outResolved) && existsSync(metaPath)) {
try {
const meta = JSON.parse(readFileSync(metaPath, "utf-8"))
if (meta.hash === hash) {
console.log(JSON.stringify({ path: outResolved, hash, status: "skipped" }))
return
}
} catch {
// stale meta, re-render
}
}
const svg = buildSvg(templateSvg, brand, { template, title, subtitle, slots, out }, __dirname)
const tmpSvg = path.join(__dirname, ".tmp-render.svg")
writeFileSync(tmpSvg, svg)
const r = spawnSync("node", [path.join(__dirname, "render.mjs"), tmpSvg, outResolved], {
encoding: "utf-8",
})
if (r.status !== 0) {
process.stderr.write(r.stderr || r.stdout || "render failed\n")
process.exit(1)
}
const meta = {
hash,
generatedAt: new Date().toISOString(),
size: 1024,
}
writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n")
console.log(JSON.stringify({ path: outResolved, hash, status: "rendered" }))
}
main()