Files
jobyssey/.agents/skills/jobsdb-search/cli/src/cli.ts
T
2026-08-26 10:35:34 -04:00

87 lines
4.2 KiB
TypeScript
Executable File

#!/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 <posting_id|url> [--format json|plain]
Search flags:
-q, --query <text> Full-text match on title, company, location (FTS5)
--company <text> Company name contains <text>
--location <text> Location text contains <text> (e.g. "New York", "San Francisco")
--country <code> location_country equals <code> (e.g. US)
--remote <list> Comma list of: remote, remote-regional, hybrid, onsite
--seniority <list> Comma list of: senior, staff_principal, manager, director, head_of, vp, c_level_founder, mid_unclassified, junior_associate, intern, unclassified
--sector <list> 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 <n> Company priority_tier <= n (1 is best)
--jobage <days> Posted within N days of the row's scrape snapshot (not today)
--page <n> 1-indexed page. Default 1
-n, --limit <n> Results per page. Default 20
--format json|table Default json
Env:
JOBSDB_PATH Override the database path
`
type Flags = Record<string, string | boolean>
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 <posting_id|url>", "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)
}