Job search tracker: CRUD, AI parse/tailor/cover via OmniRoute, legacy import
This commit is contained in:
169
scripts/import-legacy.ts
Normal file
169
scripts/import-legacy.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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());
|
||||
Reference in New Issue
Block a user