mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 16:46:24 +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
@@ -71,10 +71,115 @@ export interface JobCard {
|
||||
companyUrl: string | null
|
||||
location: string | null
|
||||
date: string | null
|
||||
deadline: string | null
|
||||
url: string
|
||||
description: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Jobindex moved its search results client-side. The /jobsoegning.json endpoint
|
||||
* now returns 204 No Content. The HTML page (/jobsoegning) embeds the full result
|
||||
* payload in a `var Stash = {...}` script blob, under
|
||||
* jobsearch/result_app -> storeData -> searchResponse -> { hitcount, results[] }.
|
||||
* This extracts and parses that blob.
|
||||
*/
|
||||
export function extractStash(html: string): any {
|
||||
const marker = "var Stash = "
|
||||
const start = html.indexOf(marker)
|
||||
if (start === -1) throw new Error("Could not locate Stash blob in jobindex HTML")
|
||||
const open = start + marker.length
|
||||
let depth = 0
|
||||
let inStr = false
|
||||
let esc = false
|
||||
let end = -1
|
||||
for (let j = open; j < html.length; j++) {
|
||||
const c = html[j]
|
||||
if (inStr) {
|
||||
if (esc) esc = false
|
||||
else if (c === "\\") esc = true
|
||||
else if (c === '"') inStr = false
|
||||
} else {
|
||||
if (c === '"') inStr = true
|
||||
else if (c === "{") depth++
|
||||
else if (c === "}") {
|
||||
depth--
|
||||
if (depth === 0) {
|
||||
end = j + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end === -1) throw new Error("Unterminated Stash blob in jobindex HTML")
|
||||
return JSON.parse(html.slice(open, end))
|
||||
}
|
||||
|
||||
function findSearchResponse(node: any): any {
|
||||
if (node && typeof node === "object") {
|
||||
if (!Array.isArray(node)) {
|
||||
if (
|
||||
node.searchResponse &&
|
||||
typeof node.searchResponse === "object" &&
|
||||
Array.isArray(node.searchResponse.results)
|
||||
) {
|
||||
return node.searchResponse
|
||||
}
|
||||
for (const key of Object.keys(node)) {
|
||||
const found = findSearchResponse(node[key])
|
||||
if (found) return found
|
||||
}
|
||||
} else {
|
||||
for (const item of node) {
|
||||
const found = findSearchResponse(item)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export interface SearchPageResult {
|
||||
total: number
|
||||
results: JobCard[]
|
||||
}
|
||||
|
||||
export function parseSearchPage(html: string): SearchPageResult {
|
||||
const stash = extractStash(html)
|
||||
const sr = findSearchResponse(stash)
|
||||
if (!sr) throw new Error("Could not locate searchResponse in jobindex Stash")
|
||||
|
||||
const results: JobCard[] = (sr.results ?? []).map((r: any): JobCard => {
|
||||
const tid: string = r.tid ?? ""
|
||||
let location: string | null = r.area ?? null
|
||||
if (!location) {
|
||||
try {
|
||||
location = r.geojson?.features?.[0]?.properties?.title ?? null
|
||||
} catch {
|
||||
location = null
|
||||
}
|
||||
}
|
||||
let deadline: string | null = null
|
||||
if (r.apply_deadline_asap) deadline = "ASAP"
|
||||
else if (typeof r.apply_deadline === "string") deadline = r.apply_deadline.slice(0, 10)
|
||||
else if (typeof r.lastdate === "string") deadline = r.lastdate
|
||||
|
||||
return {
|
||||
id: tid,
|
||||
title: r.headline ?? "",
|
||||
company: r.company?.name ?? r.companytext ?? null,
|
||||
companyUrl: r.company?.homeurl ?? null,
|
||||
location,
|
||||
date: r.firstdate ?? null,
|
||||
deadline,
|
||||
url: tid ? `${BASE_URL}/jobannonce/${tid}` : (r.share_url ?? r.url ?? ""),
|
||||
description: null,
|
||||
}
|
||||
})
|
||||
|
||||
const total = typeof sr.hitcount === "number" ? sr.hitcount : results.length
|
||||
return { total, results }
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode HTML entities in text
|
||||
*/
|
||||
@@ -181,6 +286,7 @@ export function parseJobCards(html: string): JobCard[] {
|
||||
companyUrl: companyUrl || null,
|
||||
location: location || null,
|
||||
date: date || null,
|
||||
deadline: null,
|
||||
url,
|
||||
description: description || null,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user