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

65 lines
3.8 KiB
TypeScript
Executable File

#!/usr/bin/env bun
// wellfound-cli: search public Wellfound role pages. Zero runtime dependencies.
import { runSearch, type SearchOpts } from "./commands/search.js"
import { runDetail } from "./commands/detail.js"
import { writeError } from "./helpers.js"
const HELP = `wellfound-cli - search Wellfound (wellfound.com) startup jobs by role and location
Usage:
cli.ts search -q "<role>" [--location "<city>"] [--remote] [--jobage <days>] [--limit <n>] [--format json|table|plain] [--no-description]
cli.ts detail <id-slug|url> [--format json|plain]
Search flags:
-q, --query <role> Role title, slugified into Wellfound's /role/ path (e.g. "senior software engineer", "backend engineer", "blockchain engineer"). Required.
-l, --location <city> City slug/name (e.g. "new-york", "San Francisco"). Omit or pass --remote for remote roles.
--remote Use the remote landing page (/role/r/<role>).
--jobage <days> Keep only jobs posted within N days (client-side, from liveStartAt).
--page <n> Only page 1 is available server-side; any other value errors.
-n, --limit <n> Cap results (client-side).
--format json|table|plain Default json.
--no-description Drop the description bodies from JSON output (cheap discovery pass).
--all-listings Keep every highlighted listing of matching startups, not just titles sharing a token with the query.
`
type Flags = Record<string, string | boolean>
function parse(argv: string[]) {
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 === "-l") { flags.location = 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 (k === "remote" || k === "no-description" || k === "all-listings" || next === undefined || next.startsWith("-")) flags[k] = true
else flags[k] = rest[++i]
continue
}
pos.push(a)
}
return { cmd, pos, flags }
}
const { cmd, pos, flags } = parse(process.argv.slice(2))
const KNOWN = new Set(["query", "location", "remote", "jobage", "page", "limit", "format", "no-description", "all-listings"])
for (const k of Object.keys(flags)) if (!KNOWN.has(k)) { writeError(`Unknown flag --${k}`, "BAD_ARG"); process.exit(1) }
function num(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 str = (v: string | boolean | undefined) => (typeof v === "string" && v ? v : undefined)
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 (!["json", "table", "plain"].includes(format)) { writeError("--format must be json, table or plain", "BAD_ARG"); process.exit(1) }
await runSearch({ query: str(flags.query) ?? "", location: str(flags.location), remote: flags.remote === true, jobage: num("jobage"), page: num("page", 1)!, limit: num("limit"), format, noDescription: flags["no-description"] === true, allListings: flags["all-listings"] === true })
} else if (cmd === "detail") {
if (!pos[0]) { writeError("detail requires an <id|url>", "NO_ID"); process.exit(1) }
const format = (str(flags.format) ?? "json") as "json" | "plain"
await runDetail(pos[0], format)
} else { writeError(`Unknown command "${cmd}"`, "BAD_CMD"); process.exit(1) }