import { Database } from "bun:sqlite" import { existsSync } from "node:fs" export const DB_PATH = process.env.JOBSDB_PATH ?? "documents/postings/jobs.db" export const REMOTE_CLASS: Record = { remote: "R1", "remote-global": "R1", "remote-regional": "R2", hybrid: "H", onsite: "O" } export const CLASS_LABEL: Record = { R1: "Remote (global)", R2: "Remote (regional)", H: "Hybrid", O: "Onsite" } export interface Row { posting_id: string company_id: string | null company_name: string | null title: string | null seniority: string | null posted_date: string | null location_text: string | null location_class: string | null location_country: string | null salary_min: string | null salary_max: string | null salary_currency: string | null url: string | null source_ats: string | null scrape_timestamp: string | null target_sector: string | null crypto_native: string | null priority_tier: number | null } export interface Job { id: string title: string | null company: string | null location: string | null date: string | null date_approximate: boolean url: string | null description: null remote: string | null seniority: string | null salary: string | null sector: string | null crypto_native: boolean priority_tier: number | null ats: string | null snapshot: string | null } export function openDb(): Database { if (!existsSync(DB_PATH)) { writeError(`database not found at ${DB_PATH} (set JOBSDB_PATH)`, "NO_DB") process.exit(1) } return new Database(DB_PATH, { readonly: true }) } /** "Posted 8 Days Ago" + scrape_timestamp -> ISO date. "30+" is a floor, flagged approximate. */ export function resolveDate(posted: string | null, scraped: string | null): { date: string | null; approximate: boolean } { if (!posted) return { date: null, approximate: false } // greenhouse/ashby/lever store ISO timestamps; workday stores relative text. if (/^\d{4}-\d{2}-\d{2}/.test(posted)) return { date: posted.slice(0, 10), approximate: false } if (!scraped) return { date: null, approximate: false } const base = new Date(scraped) if (isNaN(base.getTime())) return { date: null, approximate: false } const m = /(\d+)(\+?)\s*Days?/i.exec(posted) let days: number let approximate = false if (/Today/i.test(posted)) days = 0 else if (/Yesterday/i.test(posted)) days = 1 else if (m) { days = Number(m[1]); approximate = m[2] === "+" } else return { date: null, approximate: false } base.setUTCDate(base.getUTCDate() - days) return { date: base.toISOString().slice(0, 10), approximate } } export function toJob(r: Row): Job { const { date, approximate } = resolveDate(r.posted_date, r.scrape_timestamp) const salary = r.salary_min && r.salary_max ? `${r.salary_min}-${r.salary_max} ${r.salary_currency ?? ""}`.trim() : null return { id: r.posting_id, title: r.title || null, company: r.company_name || null, location: r.location_text || null, date, date_approximate: approximate, url: r.url || null, description: null, remote: r.location_class ? CLASS_LABEL[r.location_class] ?? null : null, seniority: r.seniority || null, salary, sector: r.target_sector || null, crypto_native: r.crypto_native === "True", priority_tier: r.priority_tier ?? null, ats: r.source_ats || null, snapshot: r.scrape_timestamp ? r.scrape_timestamp.slice(0, 10) : null, } } export function writeError(error: string, code: string): void { process.stderr.write(JSON.stringify({ error, code }) + "\n") } export function renderTable(jobs: Job[]): string { if (jobs.length === 0) return "No results." const rows = jobs.map((j) => [ (j.title ?? "").slice(0, 44).padEnd(44), (j.company ?? "-").slice(0, 22).padEnd(22), (j.location ?? "-").slice(0, 22).padEnd(22), (j.remote ?? "-").slice(0, 16).padEnd(16), j.date ? j.date + (j.date_approximate ? "~" : " ") : "-".padEnd(11), j.url ?? "", ].join(" "), ) return rows.join("\n") }