mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 16:46:24 +00:00
* feat(freehire-search): add country-agnostic freehire.dev aggregator skill
Adds a portal-search skill over the freehire.dev public JSON API — an
open-source IT job aggregator normalizing ~50 ATS platforms across many
markets into one schema. Like linkedin-search it is country-agnostic and
zero-dependency (plain bun + fetch), but it queries a JSON API rather than
scraping HTML, so results carry structured facets (skills/seniority/region).
Honors the portal-skill contract: search + detail commands, --format
json|table|plain, stderr JSON errors with exit 1, backoff on 429/5xx. Reads
are public (no API key) — the same zero-signup bar as linkedin-search. The
hosted-service dependency (best-effort, no SLA) is labeled prominently in
SKILL.md, and FREEHIRE_API_URL swaps the base URL for a self-hosted backend.
Scoped tech-first: triggers cover software/data/engineering roles, where the
faceted filtering is strong; non-tech coverage exists but is still maturing.
Network-free tests (mocked fetch + pure reshape/parse functions); CI matrix
updated to typecheck the new CLI.
* refactor(freehire-search): clarity pass on cli flag parsing
No behavior change. Replace a nested ternary and a comma-operator side effect
in a ternary with explicit if/else, and fix a comment that described facets
while sitting on the alias map.
* refactor(freehire-search): tighten to boundary contracts, trim comments
- Validate/normalize at boundaries, trust the declared types inside: drop the
redundant '?? []' guards on facet arrays the wire contract already guarantees,
and the re-filter in buildQuery (commaList already stripped empties).
- Model enrichment as always-present (an unenriched job serializes it as {}),
removing the '?? {}' guard.
- Replace the positional table-row builder with a declarative column list; add a
shared shortDate and a labeled-field helper for detail's plain output.
- Extract stringFlag for the string-or-bare-boolean flags (--remote/--query/...).
- Dedup the response parse in apiGet to a single tolerant read (drop safeJson).
- SKILL.md: document partial data + the 'none' unspecified-region facet.
- Trim restating comments to the reference skills' density.
41 lines
1.0 KiB
TypeScript
41 lines
1.0 KiB
TypeScript
import { join } from "path";
|
|
|
|
const CLI_PATH = join(import.meta.dir, "../src/cli.ts");
|
|
|
|
export interface CLIResult {
|
|
stdout: string;
|
|
stderr: string;
|
|
exitCode: number;
|
|
}
|
|
|
|
export async function runCLI(args: string[], env: Record<string, string> = {}): Promise<CLIResult> {
|
|
const proc = Bun.spawn(["bun", "run", CLI_PATH, ...args], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: { ...process.env, ...env },
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
]);
|
|
|
|
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
}
|
|
|
|
export function parseJSON<T = unknown>(result: CLIResult): T {
|
|
if (result.exitCode !== 0) {
|
|
throw new Error(
|
|
`CLI exited with code ${result.exitCode}. stderr: ${result.stderr}`
|
|
);
|
|
}
|
|
try {
|
|
return JSON.parse(result.stdout) as T;
|
|
} catch {
|
|
throw new Error(
|
|
`Failed to parse JSON. stdout: ${result.stdout}\nstderr: ${result.stderr}`
|
|
);
|
|
}
|
|
}
|