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:
2026-06-03 16:01:32 +02:00
parent bcb6ae5ea3
commit fb67c38edd
40 changed files with 1472 additions and 806 deletions
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bun
import { isEnglishContent, NON_ENGLISH_RE, JUNIOR_TITLE_RE } from "../fetch.ts";
const CSV = "./jobs.csv";
const existingLinks = new Set<string>();
try {
const text = await Bun.file(CSV).text();
for (const line of text.split("\n").slice(1)) {
const url = line.match(/"(https?:\/\/[^"]+)"/)?.[1];
if (url) existingLinks.add(url);
}
} catch {}
const RUBY_KEYWORDS = [
"Ruby on Rails", "Rails Engineer", "Ruby Engineer",
"Rails Developer", "Ruby Developer",
].join(" OR ");
const searches = [
{ label: "United States Remote (Ruby)", location: "United States", workType: "2" },
];
const RUBY_RE = /\bruby\b|\brails\b/i;
interface Job { id: string; title: string; company: string; location: string; link: string; postedAt: string }
function unescape(s: string) {
return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
.replace(/&#39;/g, "'").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim();
}
async function fetchPage(location: string, workType: string, start: number): Promise<Job[]> {
const params = new URLSearchParams({
keywords: RUBY_KEYWORDS,
location,
f_TPR: "r2592000",
f_WT: workType,
start: String(start),
});
const url = `https://www.linkedin.com/jobs/search/?${params}`;
const res = await fetch(url, {
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" }
});
const html = await res.text();
const jobs: Job[] = [];
const cardRe = /<div[^>]*class="[^"]*base-search-card[^"]*"[^>]*data-entity-urn="urn:li:jobPosting:(\d+)"[^>]*>([\s\S]*?)(?=<div[^>]*class="[^"]*base-search-card[^"]*"[^>]*data-entity-urn|<\/ul>)/g;
let m;
while ((m = cardRe.exec(html)) !== null) {
const id = m[1];
const chunk = m[2];
const titleM = chunk.match(/class="[^"]*base-search-card__title[^"]*"[^>]*>\s*([\s\S]*?)\s*<\/h3>/);
const companyM = chunk.match(/class="[^"]*base-search-card__subtitle[^"]*"[\s\S]*?<a[^>]*>\s*([\s\S]*?)\s*<\/a>/);
const locationM = chunk.match(/class="[^"]*job-search-card__location[^"]*"[^>]*>\s*([\s\S]*?)\s*<\//);
const postedM = chunk.match(/datetime="([^"]+)"/);
const title = titleM ? unescape(titleM[1].replace(/<[^>]+>/g, "")) : "";
const company = companyM ? unescape(companyM[1].replace(/<[^>]+>/g, "")) : "";
const location = locationM ? unescape(locationM[1]) : "";
const postedAt = postedM ? postedM[1] : "";
if (!title || !id) continue;
jobs.push({ id, title, company, location, link: `https://www.linkedin.com/jobs/view/${id}/`, postedAt });
}
return jobs;
}
const newJobs: Array<Job & { search: string }> = [];
const seenIds = new Set<string>();
for (const s of searches) {
console.log(`\n[${s.label}]`);
let totalFound = 0, added = 0;
for (let page = 0; page < 4; page++) {
const jobs = await fetchPage(s.location, s.workType, page * 25);
if (jobs.length === 0) { console.log(` page ${page + 1} empty, stopping`); break; }
console.log(` page ${page + 1} … ${jobs.length} cards`);
totalFound += jobs.length;
for (const job of jobs) {
if (existingLinks.has(job.link) || seenIds.has(job.id)) continue;
if (NON_ENGLISH_RE.test(job.title) || JUNIOR_TITLE_RE.test(job.title)) continue;
if (!RUBY_RE.test(job.title)) continue;
seenIds.add(job.id);
process.stdout.write(` checking: ${job.title} @ ${job.company} (${job.location}) … `);
const english = await isEnglishContent(job.id);
if (!english) { console.log("dropped (non-English)"); continue; }
console.log("ok");
newJobs.push({ ...job, search: s.label });
added++;
}
}
console.log(` ${totalFound} total, ${added} new`);
}
console.log(`\n=== ${newJobs.length} Ruby remote jobs ===\n`);
for (const j of newJobs) {
console.log(`${j.title} | ${j.company} | ${j.location} | ${j.link}`);
}