- move temp scripts to scripts/, cv files to cv/, jobs.csv to results/ - update index.ts and recheck.ts CSV path to results/jobs.csv - fix imports in scripts/ to use ../config.ts and ../fetch.ts - update CLAUDE.md and README.md to reflect new structure - add plans/ with LinkedIn post drafts - update .gitignore: ignore results/, jobs/, cv binaries, .DS_Store
88 lines
4.4 KiB
TypeScript
88 lines
4.4 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { KEYWORDS, fetchJobs, isEnglishContent } from "../fetch.ts";
|
|
import type { SearchConfig } from "../config.ts";
|
|
|
|
// Re-export KEYWORDS isn't in fetch.ts, import from config
|
|
import { KEYWORDS as CFG_KEYWORDS } from "../config.ts";
|
|
|
|
const HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
"Accept-Language": "en-US,en;q=0.9",
|
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
};
|
|
|
|
const SAAS_RE = /\bsaas\b/i;
|
|
|
|
async function hasSaaS(id: string): Promise<boolean> {
|
|
try {
|
|
await Bun.sleep(350);
|
|
const res = await fetch(`https://www.linkedin.com/jobs/view/${id}/`, { headers: HEADERS });
|
|
const html = await res.text();
|
|
const m = html.match(/class="show-more-less-html__markup[^"]*">([\s\S]{0,8000}?)<\/div>/);
|
|
const text = m ? m[1].replace(/<[^>]+>/g, " ") : html.replace(/<[^>]+>/g, " ").slice(0, 5000);
|
|
return SAAS_RE.test(text);
|
|
} catch { return false; }
|
|
}
|
|
|
|
const RUBY_KEYWORDS = [
|
|
"Ruby on Rails", "Rails Engineer", "Rails Developer",
|
|
"Ruby Engineer", "Ruby Developer", "Ruby",
|
|
].join(" OR ");
|
|
|
|
// --- Part 1: Ruby/Rails in Switzerland ---
|
|
const rubySearches: SearchConfig[] = [
|
|
{ label: "Basel Ruby (onsite+hybrid)", keywords: RUBY_KEYWORDS, geoId: "103383283", location: "Basel", workType: ["onsite", "hybrid"], timePosted: "month" },
|
|
{ label: "Basel Ruby (remote)", keywords: RUBY_KEYWORDS, geoId: "103383283", location: "Basel", workType: ["remote"], timePosted: "month" },
|
|
{ label: "Zürich Ruby (onsite+hybrid)",keywords: RUBY_KEYWORDS, geoId: "102436504", location: "Zürich, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" },
|
|
{ label: "Zürich Ruby (remote)", keywords: RUBY_KEYWORDS, geoId: "102436504", location: "Zürich, Switzerland", workType: ["remote"], timePosted: "month" },
|
|
{ label: "Switzerland Ruby (remote)", keywords: RUBY_KEYWORDS, location: "Switzerland", workType: ["remote"], timePosted: "month" },
|
|
{ label: "Bern Ruby", keywords: RUBY_KEYWORDS, location: "Bern, Switzerland", workType: ["onsite", "hybrid", "remote"], timePosted: "month" },
|
|
];
|
|
|
|
// --- Part 2: SaaS-experience jobs in Switzerland ---
|
|
const saasSearches: SearchConfig[] = [
|
|
{ label: "Basel SaaS", keywords: CFG_KEYWORDS, geoId: "103383283", location: "Basel", workType: ["onsite", "hybrid"], timePosted: "month" },
|
|
{ label: "Basel Metro SaaS", keywords: CFG_KEYWORDS, geoId: "90009885", location: "Basel Metropolitan Area", workType: ["onsite", "hybrid"], timePosted: "month" },
|
|
{ label: "Zürich SaaS", keywords: CFG_KEYWORDS, geoId: "102436504", location: "Zürich, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" },
|
|
{ label: "Switzerland SaaS remote", keywords: CFG_KEYWORDS, location: "Switzerland", workType: ["remote"], timePosted: "month" },
|
|
];
|
|
|
|
type Job = { id: string; title: string; company: string; link: string; search: string };
|
|
|
|
async function runSearches(searches: SearchConfig[], label: string, filterFn?: (id: string) => Promise<boolean>) {
|
|
const results: Job[] = [];
|
|
const seen = new Set<string>();
|
|
console.log(`\n${"=".repeat(50)}\n${label}\n${"=".repeat(50)}`);
|
|
|
|
for (const config of searches) {
|
|
console.log(`\n[${config.label}]`);
|
|
const jobs = await fetchJobs(config);
|
|
if (jobs.length === 0) { console.log(" no jobs found"); continue; }
|
|
|
|
let added = 0;
|
|
for (const job of jobs) {
|
|
if (seen.has(job.id)) continue;
|
|
seen.add(job.id);
|
|
if (filterFn) {
|
|
process.stdout.write(` ${job.title} @ ${job.company} … `);
|
|
const pass = await filterFn(job.id);
|
|
if (!pass) { console.log("no SaaS"); continue; }
|
|
console.log("SaaS ✓");
|
|
}
|
|
results.push({ ...job, search: config.label });
|
|
added++;
|
|
}
|
|
console.log(` => ${added} added`);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
const rubyJobs = await runSearches(rubySearches, "RUBY / RAILS IN SWITZERLAND");
|
|
const saasJobs = await runSearches(saasSearches, "SaaS-EXPERIENCE JOBS IN SWITZERLAND", hasSaaS);
|
|
|
|
console.log(`\n\n${"=".repeat(50)}\nRUBY RESULTS: ${rubyJobs.length}\n${"=".repeat(50)}`);
|
|
for (const j of rubyJobs) console.log(`${j.title} | ${j.company} | ${j.id} | ${j.link}`);
|
|
|
|
console.log(`\n${"=".repeat(50)}\nSaaS RESULTS: ${saasJobs.length}\n${"=".repeat(50)}`);
|
|
for (const j of saasJobs) console.log(`${j.title} | ${j.company} | ${j.id} | ${j.link}`);
|