mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix: restore Danish CLI commands/ and tsconfig files dropped by old .gitignore (#53)
The pre-#21 .gitignore's unanchored 'commands/' rule silently excluded .agents/skills/*/cli/src/commands/ (and the tsconfigs) from the initial release, so every clone's four Danish portal CLIs failed on import with 'Cannot find module ./commands/search.js'. #21 fixed the rule but the files were never restored - git history has no trace of them. Restored from the maintainer's working copies, including the updated jobindex helpers.ts (Jobindex moved search results from the JSON endpoint, which now returns 204, into an embedded HTML Stash blob). Verified: all four CLIs typecheck and return live results with their documented flags. Surfaced while reviewing #52. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
fce2cf23c0
commit
f3d4448cca
@@ -0,0 +1,111 @@
|
||||
import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { apiFetch, writeError } from "../helpers.js"
|
||||
|
||||
interface AutocompleteItem {
|
||||
id: string
|
||||
text: string
|
||||
value: number
|
||||
category: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface AutocompleteGroup {
|
||||
title: string
|
||||
items: AutocompleteItem[]
|
||||
}
|
||||
|
||||
export const autocomplete = defineCommand({
|
||||
name: "autocomplete",
|
||||
description: "Suggest job titles and categories for a query",
|
||||
options: {
|
||||
query: option(z.string().optional(), {
|
||||
description: "Search text to autocomplete (required)",
|
||||
}),
|
||||
limit: option(z.coerce.number().optional(), {
|
||||
description: "Cap total suggestions returned",
|
||||
}),
|
||||
format: option(z.enum(["json", "table", "plain"]).default("json"), {
|
||||
description: "Output format: json, table, plain",
|
||||
}),
|
||||
},
|
||||
handler: async ({ flags, signal }) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
if (!flags.query) {
|
||||
writeError("--query is required", "MISSING_REQUIRED")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await apiFetch<AutocompleteGroup[]>("/api/search/autocomplete", {
|
||||
q: flags.query,
|
||||
})
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
const queryLower = flags.query.toLowerCase()
|
||||
|
||||
// Filter groups: only include items whose text matches the query (API always returns all categories)
|
||||
// This ensures a nonsense query returns []
|
||||
const filtered = raw
|
||||
.map((g) => ({
|
||||
title: g.title,
|
||||
items: (g.items ?? []).filter((item) =>
|
||||
item.text.toLowerCase().includes(queryLower)
|
||||
),
|
||||
}))
|
||||
.filter((g) => g.items.length > 0)
|
||||
|
||||
let result = filtered
|
||||
|
||||
if (flags.limit !== undefined) {
|
||||
// Apply limit across all groups, distributing across groups
|
||||
let remaining = flags.limit
|
||||
result = []
|
||||
for (const group of filtered) {
|
||||
if (remaining <= 0) break
|
||||
const items = group.items.slice(0, remaining)
|
||||
remaining -= items.length
|
||||
if (items.length > 0) {
|
||||
result.push({ title: group.title, items })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.format === "json") {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
} else if (flags.format === "table") {
|
||||
outputTable(result)
|
||||
} else {
|
||||
outputPlain(result)
|
||||
}
|
||||
} catch (err) {
|
||||
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function outputTable(data: AutocompleteGroup[]): void {
|
||||
console.log("category id text value slug")
|
||||
for (const group of data) {
|
||||
for (const item of group.items) {
|
||||
const cat = item.category.padEnd(10)
|
||||
const id = item.id.substring(0, 20).padEnd(20)
|
||||
const text = item.text.substring(0, 32).padEnd(32)
|
||||
const value = String(item.value).padEnd(6)
|
||||
const slug = item.slug
|
||||
console.log(`${cat} ${id} ${text} ${value} ${slug}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function outputPlain(data: AutocompleteGroup[]): void {
|
||||
for (const group of data) {
|
||||
console.log(`=== ${group.title} ===`)
|
||||
for (const item of group.items) {
|
||||
console.log(` ${item.text} (${item.category}, id=${item.value}, slug=${item.slug})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { apiFetch, writeError } from "../helpers.js"
|
||||
|
||||
interface Category {
|
||||
id: number
|
||||
title: string
|
||||
helpText: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export const categories = defineCommand({
|
||||
name: "categories",
|
||||
description: "List all job categories with live counts",
|
||||
options: {
|
||||
limit: option(z.coerce.number().optional(), {
|
||||
description: "Cap number of categories returned",
|
||||
}),
|
||||
format: option(z.enum(["json", "table", "plain"]).default("json"), {
|
||||
description: "Output format: json, table, plain",
|
||||
}),
|
||||
},
|
||||
handler: async ({ flags, signal }) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
try {
|
||||
let data = await apiFetch<Category[]>("/api/categorycount/getcounts")
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
if (flags.limit !== undefined) {
|
||||
data = data.slice(0, flags.limit)
|
||||
}
|
||||
|
||||
if (flags.format === "json") {
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
} else if (flags.format === "table") {
|
||||
outputTable(data)
|
||||
} else {
|
||||
outputPlain(data)
|
||||
}
|
||||
} catch (err) {
|
||||
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function outputTable(data: Category[]): void {
|
||||
console.log("id title count")
|
||||
for (const cat of data) {
|
||||
const id = String(cat.id).padEnd(9)
|
||||
const title = cat.title.substring(0, 48).padEnd(48)
|
||||
const count = String(cat.count)
|
||||
console.log(`${id} ${title} ${count}`)
|
||||
}
|
||||
}
|
||||
|
||||
function outputPlain(data: Category[]): void {
|
||||
for (const cat of data) {
|
||||
console.log(`id: ${cat.id}`)
|
||||
console.log(`title: ${cat.title}`)
|
||||
console.log(`helpText: ${cat.helpText}`)
|
||||
console.log(`count: ${cat.count}`)
|
||||
console.log("")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { parse } from "node-html-parser"
|
||||
import { BASE_URL, writeError } from "../helpers.js"
|
||||
|
||||
interface JsonLdJobPosting {
|
||||
"@context"?: string
|
||||
"@type"?: string
|
||||
title?: string
|
||||
datePosted?: string
|
||||
validThrough?: string
|
||||
employmentType?: string | string[]
|
||||
hiringOrganization?: {
|
||||
"@type"?: string
|
||||
name?: string
|
||||
logo?: string
|
||||
}
|
||||
jobLocation?: {
|
||||
"@type"?: string
|
||||
address?: {
|
||||
"@type"?: string
|
||||
streetAddress?: string
|
||||
addressLocality?: string
|
||||
addressRegion?: string
|
||||
postalCode?: string
|
||||
addressCountry?: string
|
||||
}
|
||||
}
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const detail = defineCommand({
|
||||
name: "detail",
|
||||
description: "Full detail for a single job posting (by slug)",
|
||||
options: {
|
||||
format: option(z.enum(["json", "plain"]).default("json"), {
|
||||
description: "Output format: json, plain",
|
||||
}),
|
||||
},
|
||||
handler: async ({ flags, positional, signal }) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
const slug = positional[0]
|
||||
if (!slug) {
|
||||
writeError("slug argument is required", "MISSING_REQUIRED")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const url = `${BASE_URL}/job/${slug}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"Accept": "text/html,application/xhtml+xml",
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 404) {
|
||||
writeError("Job not found", "NOT_FOUND")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
writeError(`API request failed: ${response.status} ${response.statusText}`, "API_ERROR")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const html = await response.text()
|
||||
const root = parse(html)
|
||||
|
||||
// Find JSON-LD script tag
|
||||
const ldJsonScripts = root.querySelectorAll('script[type="application/ld+json"]')
|
||||
let jobPosting: JsonLdJobPosting | null = null
|
||||
|
||||
for (const script of ldJsonScripts) {
|
||||
try {
|
||||
const parsed = JSON.parse(script.text)
|
||||
if (parsed["@type"] === "JobPosting") {
|
||||
jobPosting = parsed
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// continue to next script
|
||||
}
|
||||
}
|
||||
|
||||
if (!jobPosting) {
|
||||
// Check for 404 by looking at page content
|
||||
const pageTitle = root.querySelector("title")?.text ?? ""
|
||||
if (pageTitle.toLowerCase().includes("404") || html.toLowerCase().includes("siden blev ikke fundet")) {
|
||||
writeError("Job not found", "NOT_FOUND")
|
||||
} else {
|
||||
writeError("Failed to parse JSON-LD from job page", "PARSE_ERROR")
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
const hiringOrg = jobPosting.hiringOrganization
|
||||
const address = jobPosting.jobLocation?.address
|
||||
|
||||
const employmentType = Array.isArray(jobPosting.employmentType)
|
||||
? jobPosting.employmentType
|
||||
: jobPosting.employmentType
|
||||
? [jobPosting.employmentType]
|
||||
: []
|
||||
|
||||
const output = {
|
||||
slug,
|
||||
url,
|
||||
title: jobPosting.title ?? "",
|
||||
datePosted: jobPosting.datePosted ?? "",
|
||||
validThrough: jobPosting.validThrough ?? null,
|
||||
employmentType,
|
||||
hiringOrganization: {
|
||||
name: hiringOrg?.name ?? "",
|
||||
logo: hiringOrg?.logo ?? null,
|
||||
},
|
||||
jobLocation: {
|
||||
streetAddress: address?.streetAddress ?? null,
|
||||
addressLocality: address?.addressLocality ?? null,
|
||||
addressRegion: address?.addressRegion ?? null,
|
||||
postalCode: address?.postalCode ?? null,
|
||||
addressCountry: address?.addressCountry ?? null,
|
||||
},
|
||||
description: jobPosting.description ?? "",
|
||||
}
|
||||
|
||||
if (flags.format === "json") {
|
||||
console.log(JSON.stringify(output, null, 2))
|
||||
} else {
|
||||
outputPlain(output)
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes("NOT_FOUND")) {
|
||||
writeError("Job not found", "NOT_FOUND")
|
||||
} else {
|
||||
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function outputPlain(data: Record<string, unknown>): void {
|
||||
console.log(`slug: ${data.slug}`)
|
||||
console.log(`url: ${data.url}`)
|
||||
console.log(`title: ${data.title}`)
|
||||
console.log(`datePosted: ${data.datePosted}`)
|
||||
console.log(`validThrough: ${data.validThrough ?? "N/A"}`)
|
||||
const empType = Array.isArray(data.employmentType) ? data.employmentType.join(", ") : "-"
|
||||
console.log(`employmentType: ${empType}`)
|
||||
const org = data.hiringOrganization as { name: string; logo: string | null }
|
||||
console.log(`company: ${org.name}`)
|
||||
const loc = data.jobLocation as {
|
||||
streetAddress: string | null
|
||||
addressLocality: string | null
|
||||
postalCode: string | null
|
||||
addressCountry: string | null
|
||||
}
|
||||
console.log(`location: ${[loc.streetAddress, loc.addressLocality, loc.postalCode, loc.addressCountry].filter(Boolean).join(", ")}`)
|
||||
console.log(`description: ${data.description}`)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { apiFetch, writeError } from "../helpers.js"
|
||||
|
||||
interface LocationItem {
|
||||
id: string
|
||||
text: string
|
||||
value: string
|
||||
category: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface LocationGroup {
|
||||
title: string
|
||||
items: LocationItem[]
|
||||
}
|
||||
|
||||
export const locations = defineCommand({
|
||||
name: "locations",
|
||||
description: "Suggest municipalities, zip codes, and regions for a query",
|
||||
options: {
|
||||
query: option(z.string().optional(), {
|
||||
description: "Location text to search (city, zip code, region) (required)",
|
||||
}),
|
||||
limit: option(z.coerce.number().optional(), {
|
||||
description: "Cap total suggestions returned",
|
||||
}),
|
||||
format: option(z.enum(["json", "table", "plain"]).default("json"), {
|
||||
description: "Output format: json, table, plain",
|
||||
}),
|
||||
},
|
||||
handler: async ({ flags, signal }) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
if (!flags.query) {
|
||||
writeError("--query is required", "MISSING_REQUIRED")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await apiFetch<LocationGroup[]>("/api/search/locations", {
|
||||
q: flags.query,
|
||||
})
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
// Filter out groups with no items
|
||||
const filtered = raw.filter((g) => g.items && g.items.length > 0)
|
||||
|
||||
let result = filtered
|
||||
|
||||
if (flags.limit !== undefined) {
|
||||
// Apply limit across all groups
|
||||
let remaining = flags.limit
|
||||
result = []
|
||||
for (const group of filtered) {
|
||||
if (remaining <= 0) break
|
||||
const items = group.items.slice(0, remaining)
|
||||
remaining -= items.length
|
||||
if (items.length > 0) {
|
||||
result.push({ title: group.title, items })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.format === "json") {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
} else if (flags.format === "table") {
|
||||
outputTable(result)
|
||||
} else {
|
||||
outputPlain(result)
|
||||
}
|
||||
} catch (err) {
|
||||
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function outputTable(data: LocationGroup[]): void {
|
||||
console.log("category id text value slug")
|
||||
for (const group of data) {
|
||||
for (const item of group.items) {
|
||||
const cat = item.category.padEnd(12)
|
||||
const id = item.id.substring(0, 22).padEnd(22)
|
||||
const text = item.text.substring(0, 28).padEnd(28)
|
||||
const value = String(item.value).padEnd(11)
|
||||
const slug = item.slug
|
||||
console.log(`${cat} ${id} ${text} ${value} ${slug}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function outputPlain(data: LocationGroup[]): void {
|
||||
for (const group of data) {
|
||||
console.log(`=== ${group.title} ===`)
|
||||
for (const item of group.items) {
|
||||
console.log(` ${item.text} (${item.category}, value=${item.value}, slug=${item.slug})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { apiPost, writeError, BASE_URL } from "../helpers.js"
|
||||
|
||||
interface ApiSearchItem {
|
||||
title: string
|
||||
companyName: string
|
||||
companyLogo: {
|
||||
key: string
|
||||
url: string
|
||||
focalPoint: { top: number; left: number } | null
|
||||
} | null
|
||||
companyLogoSvgMarkup: string | null
|
||||
overlayColor: string | null
|
||||
companyAddress: string
|
||||
jobTypes: string[]
|
||||
boostJob: boolean
|
||||
publishedDate: string
|
||||
applicationDeadline: string | null
|
||||
url: string
|
||||
coverImage: {
|
||||
key: string
|
||||
url: string
|
||||
focalPoint: { top: number; left: number } | null
|
||||
} | null
|
||||
silhouetteLogo: boolean
|
||||
}
|
||||
|
||||
interface ApiSearchResponse {
|
||||
items: ApiSearchItem[]
|
||||
currentPage: number
|
||||
totalItems: number
|
||||
itemsPrPage: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
function normalizeItem(item: ApiSearchItem): Record<string, unknown> {
|
||||
const relativeUrl = item.url
|
||||
const fullUrl = relativeUrl.startsWith("http")
|
||||
? relativeUrl
|
||||
: `${BASE_URL}${relativeUrl}`
|
||||
// Extract slug from url path: /job/<slug>
|
||||
const slug = relativeUrl.replace(/^\/job\//, "")
|
||||
|
||||
const companyLogo = item.companyLogo
|
||||
? {
|
||||
key: item.companyLogo.key,
|
||||
url: item.companyLogo.url.startsWith("http")
|
||||
? item.companyLogo.url
|
||||
: `${BASE_URL}${item.companyLogo.url}`,
|
||||
focalPoint: item.companyLogo.focalPoint,
|
||||
}
|
||||
: null
|
||||
|
||||
const coverImage = item.coverImage
|
||||
? {
|
||||
key: item.coverImage.key,
|
||||
url: item.coverImage.url.startsWith("http")
|
||||
? item.coverImage.url
|
||||
: `${BASE_URL}${item.coverImage.url}`,
|
||||
focalPoint: item.coverImage.focalPoint,
|
||||
}
|
||||
: null
|
||||
|
||||
return {
|
||||
title: item.title,
|
||||
companyName: item.companyName,
|
||||
companyLogo,
|
||||
companyLogoSvgMarkup: item.companyLogoSvgMarkup ?? null,
|
||||
overlayColor: item.overlayColor ?? null,
|
||||
companyAddress: item.companyAddress,
|
||||
jobTypes: item.jobTypes,
|
||||
boostJob: item.boostJob,
|
||||
publishedDate: item.publishedDate,
|
||||
applicationDeadline: item.applicationDeadline ?? null,
|
||||
url: fullUrl,
|
||||
slug,
|
||||
coverImage,
|
||||
silhouetteLogo: item.silhouetteLogo,
|
||||
}
|
||||
}
|
||||
|
||||
export const search = defineCommand({
|
||||
name: "search",
|
||||
description: "Search job listings with filters",
|
||||
options: {
|
||||
text: option(z.string().optional(), {
|
||||
description: "Free-text keyword search (job title, keyword)",
|
||||
}),
|
||||
category: option(z.coerce.number().optional(), {
|
||||
description: "Category ID",
|
||||
}),
|
||||
"jobtitle-id": option(z.coerce.number().optional(), {
|
||||
description: "Job title ID from autocomplete results",
|
||||
}),
|
||||
municipality: option(z.string().optional(), {
|
||||
description: "Municipality name, e.g. Odense, København",
|
||||
}),
|
||||
zip: option(z.string().optional(), {
|
||||
description: "Zip code, e.g. 5000",
|
||||
}),
|
||||
region: option(z.string().optional(), {
|
||||
description: "Region name",
|
||||
}),
|
||||
"job-type": option(z.string().optional(), {
|
||||
description: "Comma-separated job types: fuldtid,deltid,fleksjob,elev,studiejob,praktik",
|
||||
}),
|
||||
page: option(z.coerce.number().default(1), {
|
||||
description: "Page number (30 items per page, server-enforced)",
|
||||
}),
|
||||
limit: option(z.coerce.number().optional(), {
|
||||
description: "Cap total results returned by CLI (client-side)",
|
||||
}),
|
||||
format: option(z.enum(["json", "table", "plain"]).default("json"), {
|
||||
description: "Output format: json, table, plain",
|
||||
}),
|
||||
},
|
||||
handler: async ({ flags, signal }) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
const filters: Array<{ type: string; value: string | number; displayText: string }> = []
|
||||
|
||||
if (flags.text) {
|
||||
filters.push({ type: "freetext", value: flags.text, displayText: flags.text })
|
||||
}
|
||||
if (flags.category !== undefined) {
|
||||
filters.push({ type: "category", value: flags.category, displayText: String(flags.category) })
|
||||
}
|
||||
if (flags["jobtitle-id"] !== undefined) {
|
||||
filters.push({ type: "jobtitle", value: flags["jobtitle-id"], displayText: String(flags["jobtitle-id"]) })
|
||||
}
|
||||
if (flags.municipality) {
|
||||
filters.push({ type: "municipality", value: flags.municipality, displayText: flags.municipality })
|
||||
}
|
||||
if (flags.zip) {
|
||||
filters.push({ type: "zip", value: flags.zip, displayText: flags.zip })
|
||||
}
|
||||
if (flags.region) {
|
||||
filters.push({ type: "region", value: flags.region, displayText: flags.region })
|
||||
}
|
||||
|
||||
const jobTypes = flags["job-type"]
|
||||
? flags["job-type"].split(",").map((t) => t.trim()).filter(Boolean)
|
||||
: []
|
||||
|
||||
const body = {
|
||||
jobTypes,
|
||||
filters,
|
||||
locationMode: "Text",
|
||||
distance: 50,
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiPost<ApiSearchResponse>(`/api/jobsearch/search/${flags.page}`, body)
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
let results = data.items.map(normalizeItem)
|
||||
if (flags.limit !== undefined) {
|
||||
results = results.slice(0, flags.limit)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
currentPage: data.currentPage,
|
||||
totalItems: data.totalItems,
|
||||
itemsPrPage: data.itemsPrPage,
|
||||
totalPages: data.totalPages,
|
||||
}
|
||||
|
||||
const output = { meta, results }
|
||||
|
||||
if (flags.format === "json") {
|
||||
console.log(JSON.stringify(output, null, 2))
|
||||
} else if (flags.format === "table") {
|
||||
outputTable(results)
|
||||
} else {
|
||||
outputPlain(results)
|
||||
}
|
||||
} catch (err) {
|
||||
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function outputTable(results: Record<string, unknown>[]): void {
|
||||
console.log("title company date url")
|
||||
for (const r of results) {
|
||||
const title = String(r.title ?? "-").substring(0, 35).padEnd(35)
|
||||
const company = String(r.companyName ?? "-").substring(0, 25).padEnd(25)
|
||||
const date = String(r.publishedDate ?? "-").padEnd(10)
|
||||
const url = String(r.url ?? "-")
|
||||
console.log(`${title} ${company} ${date} ${url}`)
|
||||
}
|
||||
}
|
||||
|
||||
function outputPlain(results: Record<string, unknown>[]): void {
|
||||
for (const r of results) {
|
||||
console.log(`title: ${r.title}`)
|
||||
console.log(`company: ${r.companyName}`)
|
||||
console.log(`address: ${r.companyAddress}`)
|
||||
console.log(`jobTypes: ${Array.isArray(r.jobTypes) ? r.jobTypes.join(", ") : "-"}`)
|
||||
console.log(`published: ${r.publishedDate}`)
|
||||
console.log(`deadline: ${r.applicationDeadline ?? "N/A"}`)
|
||||
console.log(`url: ${r.url}`)
|
||||
console.log("")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ESNext"],
|
||||
"types": ["bun-types"],
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*", "tests/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user