/** * Synchronize the YAML corpus under ../docs into the local D1 database. * * Usage: bun scripts/sync.ts [--remote] * * Applies migrations, regenerates seed.generated.sql from the YAML files, * and executes it against D1 (local by default). */ import { execSync } from "node:child_process"; import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; import yaml from "js-yaml"; const APP = join(import.meta.dirname, ".."); const DOCS = join(APP, "..", "docs"); const TARGET = process.argv.includes("--remote") ? "--remote" : "--local"; // ---------- helpers ---------- const sql = (v: string | number | null | undefined): string => v == null ? "NULL" : typeof v === "number" ? String(v) : `'${v.replace(/'/g, "''")}'`; /** js-yaml parses ISO dates into Date objects; normalize everything to strings. */ function iso(v: unknown): string | null { if (v == null) return null; if (v instanceof Date) return v.toISOString().slice(0, 10); return String(v); } /** Split a `description` block scalar into its "- " bullets (with wrapped-line continuation). */ function bulletsOf(desc: unknown): string[] { if (typeof desc !== "string") return []; const out: string[] = []; for (const line of desc.split("\n")) { const t = line.trim(); if (t.startsWith("- ")) out.push(t.slice(2).trim()); else if (t && out.length > 0) out[out.length - 1] += ` ${t}`; } return out; } function loadYaml(path: string): Record { return yaml.load(readFileSync(path, "utf8")) as Record; } // ---------- build statements ---------- const stmts: string[] = [ "DELETE FROM profile;", "DELETE FROM summaries;", "DELETE FROM experience;", "DELETE FROM projects;", "DELETE FROM bullets;", "DELETE FROM item_skills;", "DELETE FROM skills;", "DELETE FROM education;", "DELETE FROM honors;", "DELETE FROM organizations;", ]; // about/profile.yml { const p = loadYaml(join(DOCS, "about/profile.yml")); stmts.push( `INSERT INTO profile (id, full_name, email, phone, website, linkedin, github, twitter) VALUES (1, ${sql( String(p["full-name"]), )}, ${sql(iso(p.email))}, ${sql(iso(p.phone))}, ${sql(iso(p.website))}, ${sql( iso(p.linkedin), )}, ${sql(iso(p.github))}, ${sql(iso(p.twitter))});`, ); for (const s of (p.summary as { role: string; summary: string }[] | undefined) ?? []) { stmts.push( `INSERT INTO summaries (role, summary) VALUES (${sql(s.role)}, ${sql(s.summary.trim())});`, ); } } // about/education.yml { const e = loadYaml(join(DOCS, "about/education.yml")); stmts.push( `INSERT INTO education (id, school, degree, field, start_date, end_date, activities) VALUES (1, ${sql( iso(e.school), )}, ${sql(iso(e.degree))}, ${sql(iso(e.field))}, ${sql(iso(e.start))}, ${sql( iso(e.end), )}, ${sql(typeof e.activities === "string" ? e.activities.trim() : null)});`, ); for (const skill of (e.skills as string[] | undefined) ?? []) { stmts.push( `INSERT INTO item_skills (parent_type, parent_slug, skill) VALUES ('education', 'education', ${sql(skill)});`, ); } } // about/honors.yml + organizations.yml (top-level lists) for (const h of yaml.load(readFileSync(join(DOCS, "about/honors.yml"), "utf8")) as Record< string, unknown >[]) { stmts.push( `INSERT INTO honors (title, date, institution) VALUES (${sql(iso(h.title))}, ${sql( iso(h.date), )}, ${sql(iso(h.institution))});`, ); } for (const o of yaml.load(readFileSync(join(DOCS, "about/organizations.yml"), "utf8")) as Record< string, unknown >[]) { stmts.push( `INSERT INTO organizations (name, url, title) VALUES (${sql(iso(o.name))}, ${sql( iso(o.url), )}, ${sql(iso(o.title))});`, ); } // skills/languages.yml (tiered map) { const tiers = loadYaml(join(DOCS, "skills/languages.yml")) as Record; let pos = 0; for (const tier of ["expert", "strong", "familiar"]) { for (const name of tiers[tier] ?? []) { stmts.push( `INSERT INTO skills (name, category, tier, position) VALUES (${sql(name)}, 'language', ${sql(tier)}, ${pos++});`, ); } } } // skills/frameworks.yml (flat list; group headers live in YAML comments) { let grp: string | null = null; let pos = 0; for (const line of readFileSync(join(DOCS, "skills/frameworks.yml"), "utf8").split("\n")) { const t = line.trim(); if (t.startsWith("#")) grp = t.replace(/^#\s*/, ""); else if (t.startsWith("- ")) stmts.push( `INSERT INTO skills (name, category, grp, position) VALUES (${sql(t.slice(2).trim())}, 'framework', ${sql(grp)}, ${pos++});`, ); } } // skills/methodology.yml (flat list) { const items = yaml.load(readFileSync(join(DOCS, "skills/methodology.yml"), "utf8")) as string[]; items.forEach((name, pos) => stmts.push( `INSERT INTO skills (name, category, position) VALUES (${sql(name)}, 'methodology', ${pos});`, ), ); } // experience/*.yml for (const file of readdirSync(join(DOCS, "experience")).filter((f) => /\.ya?ml$/.test(f)).sort()) { const slug = file.replace(/\.ya?ml$/, ""); const e = loadYaml(join(DOCS, "experience", file)); stmts.push( `INSERT INTO experience (slug, title, company, start_date, end_date, location, profile_heading) VALUES (${sql( slug, )}, ${sql(iso(e.title))}, ${sql(iso(e.company))}, ${sql(iso(e.start))}, ${sql( iso(e.end), )}, ${sql(iso(e.location))}, ${sql(iso(e["profile-heading"]))});`, ); bulletsOf(e.description).forEach((text, position) => stmts.push( `INSERT INTO bullets (parent_type, parent_slug, position, text) VALUES ('experience', ${sql(slug)}, ${position}, ${sql(text)});`, ), ); for (const skill of (e.skills as string[] | undefined) ?? []) { stmts.push( `INSERT INTO item_skills (parent_type, parent_slug, skill) VALUES ('experience', ${sql(slug)}, ${sql(skill)});`, ); } } // projects/*.yml for (const file of readdirSync(join(DOCS, "projects")).filter((f) => /\.ya?ml$/.test(f)).sort()) { const slug = file.replace(/\.ya?ml$/, ""); const p = loadYaml(join(DOCS, "projects", file)); stmts.push( `INSERT INTO projects (slug, title, start_date, end_date, url, associated_with) VALUES (${sql( slug, )}, ${sql(iso(p.title))}, ${sql(iso(p.start))}, ${sql(iso(p.end))}, ${sql( iso(p["project-url"]), )}, ${sql(typeof p["associated-with"] === "string" ? basename(p["associated-with"]).replace(/\.ya?ml$/, "") : null)});`, ); bulletsOf(p.description).forEach((text, position) => stmts.push( `INSERT INTO bullets (parent_type, parent_slug, position, text) VALUES ('project', ${sql(slug)}, ${position}, ${sql(text)});`, ), ); for (const skill of (p.skills as string[] | undefined) ?? []) { stmts.push( `INSERT INTO item_skills (parent_type, parent_slug, skill) VALUES ('project', ${sql(slug)}, ${sql(skill)});`, ); } } // ---------- execute ---------- const seedPath = join(APP, "seed.generated.sql"); writeFileSync(seedPath, `${stmts.join("\n")}\n`); console.log(`Wrote ${stmts.length} statements to seed.generated.sql`); execSync(`bunx wrangler d1 migrations apply cv-tailor ${TARGET}`, { cwd: APP, stdio: "inherit" }); execSync(`bunx wrangler d1 execute cv-tailor ${TARGET} --file seed.generated.sql`, { cwd: APP, stdio: "inherit", }); console.log("Sync complete.");