mirror of
https://github.com/prdlk/jobyssey.git
synced 2026-11-08 16:07:38 +00:00
87 lines
4.2 KiB
TypeScript
Executable File
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)
|
|
}
|