170 lines
6.7 KiB
TypeScript
170 lines
6.7 KiB
TypeScript
/**
|
||
* One-time import of the legacy markdown job files + resumes into the DB.
|
||
* Usage: npx tsx scripts/import-legacy.ts
|
||
*/
|
||
import { PrismaClient } from "@prisma/client";
|
||
import matter from "gray-matter";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import os from "node:os";
|
||
|
||
const prisma = new PrismaClient();
|
||
const LEGACY = path.join(os.homedir(), "development/personal/ai-job-search");
|
||
|
||
function section(body: string, name: string): string | undefined {
|
||
const re = new RegExp(`##\\s*${name}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "i");
|
||
return body.match(re)?.[1]?.trim();
|
||
}
|
||
|
||
function firstMatch(body: string, re: RegExp): string | undefined {
|
||
return body.match(re)?.[1]?.trim();
|
||
}
|
||
|
||
async function importJobs() {
|
||
const dir = path.join(LEGACY, "jobs");
|
||
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
||
for (const file of files) {
|
||
const raw = fs.readFileSync(path.join(dir, file), "utf8");
|
||
const { data, content } = matter(raw);
|
||
const title = (data.title as string) ?? file.replace(/\.md$/, "");
|
||
const company = (data.company as string) ?? "Unknown";
|
||
const existing = await prisma.job.findFirst({ where: { title, company } });
|
||
if (existing) {
|
||
console.log(`skip (exists): ${file}`);
|
||
continue;
|
||
}
|
||
const url = firstMatch(content, /\*\*Link:\*\*\s*(\S+)/);
|
||
const applyUrl = firstMatch(content, /\*\*Apply(?: link)?:\*\*\s*(\S+)/);
|
||
const details = section(content, "Details");
|
||
const payLine = details ? firstMatch(details, /\*\*Pay(?:\s*Range)?:\*\*\s*(.+)/) : undefined;
|
||
const location = details ? firstMatch(details, /\*\*Location:\*\*\s*(.+)/) : undefined;
|
||
|
||
// crude pay parse: "$15-20/hr", "₱150-170k/mo", "$65-120/hr"
|
||
let payMin: number | undefined, payMax: number | undefined, payCurrency: string | undefined, payPeriod: string | undefined;
|
||
if (payLine) {
|
||
const m = payLine.match(/([$₱P])\s*([\d,.]+)\s*[-–]\s*([\d,.]+)(k?)\s*\/?\s*(hr|hour|mo|month|yr|year)?/i);
|
||
if (m) {
|
||
const mult = m[4] === "k" ? 1000 : 1;
|
||
payMin = parseFloat(m[2].replace(/,/g, "")) * mult;
|
||
payMax = parseFloat(m[3].replace(/,/g, "")) * mult;
|
||
payCurrency = m[1] === "$" ? "USD" : "PHP";
|
||
payPeriod = /hr|hour/i.test(m[5] ?? "") ? "hour" : /mo/i.test(m[5] ?? "") ? "month" : /yr|year/i.test(m[5] ?? "") ? "year" : undefined;
|
||
}
|
||
}
|
||
|
||
const phEligibility = /philippines|ph eligible|170\+ countries|worldwide/i.test(content) ? "yes" : "unclear";
|
||
const status = (data.status as string)?.replace(/\s+/g, "-") === "not-applied" ? "prospect" : "prospect";
|
||
|
||
await prisma.job.create({
|
||
data: {
|
||
title,
|
||
company,
|
||
source: file.replace(/\.md$/, ""),
|
||
url,
|
||
applyUrl,
|
||
payMin,
|
||
payMax,
|
||
payCurrency,
|
||
payPeriod,
|
||
location,
|
||
remoteType: "remote",
|
||
phEligibility,
|
||
description: section(content, "Description"),
|
||
requirements: section(content, "Requirements"),
|
||
notes: section(content, "Notes") ?? section(content, "Tailoring notes"),
|
||
status,
|
||
},
|
||
});
|
||
console.log(`imported: ${file}`);
|
||
}
|
||
}
|
||
|
||
async function importResumes() {
|
||
const base = path.join(LEGACY, "resume/resume.md");
|
||
if (fs.existsSync(base)) {
|
||
const md = fs.readFileSync(base, "utf8");
|
||
const exists = await prisma.resume.findFirst({ where: { kind: "base" } });
|
||
if (!exists) {
|
||
await prisma.resume.create({ data: { name: "Base Resume", kind: "base", markdown: md } });
|
||
console.log("imported: base resume");
|
||
}
|
||
}
|
||
const tailoredDir = path.join(LEGACY, "resume/tailored");
|
||
if (fs.existsSync(tailoredDir)) {
|
||
for (const f of fs.readdirSync(tailoredDir).filter((f) => f.endsWith(".md"))) {
|
||
const name = `Tailored: ${f.replace(/\.md$/, "")}`;
|
||
const exists = await prisma.resume.findFirst({ where: { name } });
|
||
if (exists) continue;
|
||
await prisma.resume.create({
|
||
data: { name, kind: "tailored", markdown: fs.readFileSync(path.join(tailoredDir, f), "utf8") },
|
||
});
|
||
console.log(`imported: tailored ${f}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function seedProfile() {
|
||
const exists = await prisma.profile.findUnique({ where: { id: "main" } });
|
||
if (exists) return;
|
||
await prisma.profile.create({
|
||
data: {
|
||
id: "main",
|
||
fullName: "Joshua C. Sarmiento",
|
||
email: "joshuacsarmiento@gmail.com",
|
||
location: "Philippines",
|
||
headline: "Software Developer / AI Engineer — Prompt Engineering, Conversational AI, Backend Systems",
|
||
summary:
|
||
"Software developer with 8+ years of experience spanning enterprise IT, full-stack web development, and applied AI: building LLM-powered APIs and RAG systems with FastAPI and Amazon Bedrock.",
|
||
skills: JSON.stringify([
|
||
"Prompt engineering", "Amazon Bedrock", "RAG", "Conversational AI", "Python", "FastAPI",
|
||
"REST API design", "Active Directory", "Full-stack web development", "Bash/PowerShell automation",
|
||
]),
|
||
workHistory: JSON.stringify([
|
||
{
|
||
position: "Project Technical Specialist III",
|
||
company: "Philippine Institute of Volcanology and Seismology (PHIVOLCS)",
|
||
unit: "Geomatics and Hazard Assessment Services Section",
|
||
start: "2025-03", end: null, supervisor: "Eric Santos",
|
||
bullets: [
|
||
"Contributed to CLUP and CDRA systems",
|
||
"Gates Handa: AI APIs with FastAPI + Amazon Bedrock, chatbot, RAG document/report analyzer",
|
||
"Design system diagrams and plans; support project management",
|
||
],
|
||
},
|
||
{
|
||
position: "Computer Programmer II",
|
||
company: "Presidential Commission for the Urban Poor (PCUP)",
|
||
unit: "IT Unit",
|
||
start: "2019-08", end: "2024-02", supervisor: "Jason Ronidel",
|
||
bullets: [
|
||
"Developed major web apps: Attendance Monitoring, Service Workflow Management, UPO Accreditation, Reservation, Directory",
|
||
"Managed commission network infrastructure and devices",
|
||
"MS Access + VBA database applications",
|
||
],
|
||
},
|
||
{
|
||
position: "IT Associate",
|
||
company: "Manulife Business Process Services",
|
||
unit: "Global Access Management (GAM) Division",
|
||
start: "2016-11", end: "2017-10", supervisor: "Emory Lopez",
|
||
bullets: [
|
||
"Automated Active Directory user management with scripts (.sh, .ps1, .bat)",
|
||
"Managed AD, mainframe, Oracle, FTP, and email accounts (Exchange, Lotus Notes, O365)",
|
||
"ServiceNow request handling; VPN token creation",
|
||
],
|
||
},
|
||
]),
|
||
},
|
||
});
|
||
console.log("seeded: profile");
|
||
}
|
||
|
||
async function main() {
|
||
await importJobs();
|
||
await importResumes();
|
||
await seedProfile();
|
||
console.log("done");
|
||
}
|
||
|
||
main().finally(() => prisma.$disconnect());
|