chore: reorganize repo structure and update docs
- 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
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bun
|
||||
import { buildUrl, isEnglishContent, NON_ENGLISH_RE, JUNIOR_TITLE_RE, JAVA_TITLE_RE, SOFTWARE_TITLE_RE } from "../fetch.ts";
|
||||
import type { SearchConfig } 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",
|
||||
};
|
||||
|
||||
function clean(raw: string): string {
|
||||
return raw.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// Same as fetch.ts parseJobs but WITHOUT the Switzerland location guard
|
||||
function parseJobs(html: string, label: string) {
|
||||
const jobs: { id: string; title: string; company: string; link: string; location: string }[] = [];
|
||||
const idRe = /data-entity-urn="urn:li:jobPosting:(\d+)"/g;
|
||||
const positions: { id: string; start: number }[] = [];
|
||||
let m;
|
||||
while ((m = idRe.exec(html)) !== null) positions.push({ id: m[1], start: m.index });
|
||||
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const { id, start } = positions[i];
|
||||
const card = html.slice(start, Math.min(positions[i + 1]?.start ?? Infinity, start + 3000));
|
||||
const titleM = card.match(/<h3 class="base-search-card__title">\s*([\s\S]*?)\s*<\/h3>/);
|
||||
if (!titleM) continue;
|
||||
const companyM = card.match(/<h4 class="base-search-card__subtitle">[\s\S]{0,300}?<a[^>]*>\s*([\s\S]*?)\s*<\/a>/);
|
||||
const locM = card.match(/job-search-card__location[^>]*>\s*([^<]+)/);
|
||||
const jobLocation = locM ? locM[1].trim() : "";
|
||||
jobs.push({ id, title: clean(titleM[1]), company: companyM ? clean(companyM[1]) : "", location: jobLocation, link: `https://www.linkedin.com/jobs/view/${id}/` });
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
|
||||
async function fetchJobsNoGeo(config: SearchConfig) {
|
||||
const baseUrl = buildUrl(config);
|
||||
const all: { id: string; title: string; company: string; location: string; link: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (let page = 0; page < 6; page++) {
|
||||
if (page > 0) await Bun.sleep(400);
|
||||
const url = `${baseUrl}&start=${page * 25}`;
|
||||
process.stdout.write(` page ${page + 1} … `);
|
||||
const res = await fetch(url, { headers: HEADERS });
|
||||
if (!res.ok) { console.error(`failed (${res.status})`); break; }
|
||||
const html = await res.text();
|
||||
const jobs = parseJobs(html, config.label)
|
||||
.filter(j => !NON_ENGLISH_RE.test(j.title))
|
||||
.filter(j => !JUNIOR_TITLE_RE.test(j.title))
|
||||
.filter(j => !JAVA_TITLE_RE.test(j.title))
|
||||
.filter(j => SOFTWARE_TITLE_RE.test(j.title));
|
||||
|
||||
let newCount = 0;
|
||||
for (const job of jobs) {
|
||||
if (!seen.has(job.id)) { seen.add(job.id); all.push(job); newCount++; }
|
||||
}
|
||||
console.log(`${jobs.length} jobs, ${newCount} new`);
|
||||
if (newCount === 0) break;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
const RUBY_KEYWORDS = [
|
||||
"Ruby on Rails",
|
||||
"Rails Engineer",
|
||||
"Rails Developer",
|
||||
"Ruby Engineer",
|
||||
"Ruby Developer",
|
||||
"Tech Lead Ruby",
|
||||
"Lead Engineer Ruby",
|
||||
"Staff Ruby",
|
||||
"Principal Ruby",
|
||||
].join(" OR ");
|
||||
|
||||
const searches: SearchConfig[] = [
|
||||
{ label: "USA Remote Ruby/Rails", keywords: RUBY_KEYWORDS, location: "United States", workType: ["remote"], timePosted: "month" },
|
||||
{ label: "USA Remote Ruby/Rails mid-senior+", keywords: RUBY_KEYWORDS, location: "United States", workType: ["remote"], seniority: ["mid-senior", "director"], timePosted: "month" },
|
||||
];
|
||||
|
||||
type Job = { id: string; title: string; company: string; location: string; link: string; search: string };
|
||||
const results: Job[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const config of searches) {
|
||||
console.log(`\n[${config.label}]`);
|
||||
const jobs = await fetchJobsNoGeo(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);
|
||||
results.push({ ...job, search: config.label });
|
||||
added++;
|
||||
}
|
||||
console.log(` => ${added} new`);
|
||||
}
|
||||
|
||||
console.log(`\n=== TOTAL: ${results.length} jobs ===\n`);
|
||||
for (const j of results) {
|
||||
console.log(`${j.title} | ${j.company} | ${j.location} | ${j.id} | ${j.link}`);
|
||||
}
|
||||
Reference in New Issue
Block a user