import { DETAIL_URL, htmlFetch, parseJobDetail, stripInvisible, writeError } from "../helpers.js"
export interface DetailOpts {
id: string
format: "json" | "plain"
}
const LINKEDIN_HOST = /(^|\.)linkedin\.com$/i
/**
* Accept a raw job ID, a job URN, a LinkedIn job-view URL (any linkedin.com
* host, with or without scheme), or a bare title slug ending in the ID.
*
* A URL on any other host is rejected. The previous pattern took the first
* 6+-digit path segment from ANY URL, so a Greenhouse or Lever apply link -
* the kind a posting's own page hands out - fetched whatever LinkedIn
* posting happened to carry that number and printed it with exit 0. Host and
* path are read through real URL parsing, so look-alike hosts
* (linkedin.com.evil.io) and userinfo tricks (linkedin.com@evil.io) fall on
* the reject side too.
*/
export function normalizeId(input: string): string | null {
const trimmed = input.trim()
const urn = trimmed.match(/urn:li:jobPosting:(\d+)/)
if (urn) return urn[1]
if (/^\d{6,}$/.test(trimmed)) return trimmed
const hasScheme = /^https?:\/\//i.test(trimmed)
if (hasScheme || /^[a-z0-9.-]+\.[a-z]{2,}(\/|$)/i.test(trimmed)) {
let parsed: URL
try {
parsed = new URL(hasScheme ? trimmed : `https://${trimmed}`)
} catch {
return null
}
if (!LINKEDIN_HOST.test(parsed.hostname)) return null
const path = parsed.pathname.match(/\/jobs\/view\/(?:[^/]*-)?(\d{6,})\/?$/)
return path ? path[1] : null
}
// A scheme- and slash-free slug such as "software-engineer-1234567890".
const slug = trimmed.match(/^[a-z0-9-]*-(\d{6,})$/i)
return slug ? slug[1] : null
}
export async function runDetail(opts: DetailOpts): Promise {
const id = normalizeId(opts.id)
if (!id) {
writeError(`Could not parse a job ID from "${opts.id}"`, "BAD_ID")
return 1
}
try {
const html = await htmlFetch(`${DETAIL_URL}/${id}`)
if (!html) {
writeError("Job not found", "NOT_FOUND")
return 1
}
const job = stripInvisible(parseJobDetail(html, id))
if (opts.format === "plain") {
const lines = [
job.title,
`${job.company || "—"} · ${job.location || "—"}`,
"",
job.seniority ? `Seniority: ${job.seniority}` : "",
job.employmentType ? `Employment: ${job.employmentType}` : "",
job.jobFunction ? `Function: ${job.jobFunction}` : "",
job.industries ? `Industries: ${job.industries}` : "",
`Status: ${job.isActive ? "ACTIVE" : "CLOSED / EXPIRED"}`,
"",
job.description || "(no description)",
"",
`URL: ${job.url}`,
].filter((l) => l !== "")
process.stdout.write(lines.join("\n") + "\n")
} else {
process.stdout.write(JSON.stringify(job, null, 2) + "\n")
}
return 0
} catch (e) {
writeError(e instanceof Error ? e.message : String(e), "DETAIL_FAILED")
return 1
}
}