#!/usr/bin/env bun // jobsdb-cli: search a local SQLite snapshot of job postings. Zero dependencies (bun:sqlite). import { runSearch, type SearchOpts } from "./commands/search.js" import { runDetail } from "./commands/detail.js" import { writeError } from "./helpers.js" const HELP = `jobsdb-cli - search the local postings snapshot (documents/postings/jobs.db) Usage: cli.ts search [flags] cli.ts detail [--format json|plain] Search flags: -q, --query Full-text match on title, company, location (FTS5) --company Company name contains --location Location text contains (e.g. "New York", "San Francisco") --country location_country equals (e.g. US) --remote Comma list of: remote, remote-regional, hybrid, onsite --seniority Comma list of: senior, staff_principal, manager, director, head_of, vp, c_level_founder, mid_unclassified, junior_associate, intern, unclassified --sector Comma list of company target_sector values (crypto, fintech-nc, ai-ml, devtools, enterprise-saas, hardware, other) --crypto-native Only companies flagged crypto-native --tier Company priority_tier <= n (1 is best) --jobage Posted within N days of the row's scrape snapshot (not today) --page 1-indexed page. Default 1 -n, --limit Results per page. Default 20 --format json|table Default json Env: JOBSDB_PATH Override the database path ` type Flags = Record function parse(argv: string[]): { cmd: string; pos: string[]; flags: Flags } { const [cmd = "", ...rest] = argv const pos: string[] = [] const flags: Flags = {} for (let i = 0; i < rest.length; i++) { const a = rest[i] if (a === "-q") { flags.query = rest[++i] ?? ""; continue } if (a === "-n") { flags.limit = rest[++i] ?? ""; continue } if (a.startsWith("--")) { const [k, inline] = a.slice(2).split("=", 2) if (inline !== undefined) { flags[k] = inline; continue } const next = rest[i + 1] if (next === undefined || next.startsWith("-")) flags[k] = true else flags[k] = rest[++i] continue } pos.push(a) } return { cmd, pos, flags } } function num(flags: Flags, name: string, dflt?: number): number | undefined { const raw = flags[name] if (raw === undefined) return dflt const n = Number(raw) if (!Number.isFinite(n) || n < 0) { writeError(`--${name} must be a non-negative number, got "${raw}"`, "BAD_ARG"); process.exit(1) } return n } const list = (v: string | boolean | undefined) => (typeof v === "string" && v ? v.split(",").map((s) => s.trim()).filter(Boolean) : undefined) const str = (v: string | boolean | undefined) => (typeof v === "string" && v ? v : undefined) const { cmd, pos, flags } = parse(process.argv.slice(2)) const KNOWN = new Set(["query", "company", "location", "country", "remote", "seniority", "sector", "crypto-native", "tier", "jobage", "page", "limit", "format"]) for (const k of Object.keys(flags)) if (!KNOWN.has(k)) { writeError(`Unknown flag --${k}`, "BAD_ARG"); process.exit(1) } if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") { process.stdout.write(HELP); process.exit(0) } if (cmd === "search") { const format = (str(flags.format) ?? "json") as SearchOpts["format"] if (format !== "json" && format !== "table") { writeError(`--format must be json or table`, "BAD_ARG"); process.exit(1) } runSearch({ query: str(flags.query), company: str(flags.company), location: str(flags.location), country: str(flags.country), remote: list(flags.remote), seniority: list(flags.seniority), sector: list(flags.sector), cryptoNative: flags["crypto-native"] === true, tier: num(flags, "tier"), jobage: num(flags, "jobage"), page: Math.max(1, num(flags, "page", 1)!), limit: Math.max(1, num(flags, "limit", 20)!), format, }) } else if (cmd === "detail") { if (!pos[0]) { writeError("detail requires a ", "NO_ID"); process.exit(1) } const format = (str(flags.format) ?? "json") as "json" | "plain" runDetail(pos[0], format) } else { writeError(`Unknown command "${cmd}"`, "BAD_CMD"); process.exit(1) }