lazy-apply 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/runner.js +70 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
6
6
|
import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
|
-
var VERSION = true ? "0.1.
|
|
9
|
+
var VERSION = true ? "0.1.5" : "dev";
|
|
10
10
|
var DEFAULT_APP = "https://lazyapply.online";
|
|
11
11
|
var HOME = join(homedir(), ".lazy-apply");
|
|
12
12
|
var CONFIG = join(HOME, "config.json");
|
package/dist/runner.js
CHANGED
|
@@ -144,7 +144,33 @@ var pick = (opts, ...prefer) => {
|
|
|
144
144
|
};
|
|
145
145
|
var or = (v, fallback) => v && v.trim() || fallback;
|
|
146
146
|
var links = (s) => [s.website, s.linkedin, s.github].filter(Boolean).join(" | ");
|
|
147
|
+
var yesNo = (o, yes) => o?.length ? pick(o, yes ? /^yes\b/i : /^no\b/i) : yes ? "Yes" : "No";
|
|
148
|
+
var countriesIn = (text) => {
|
|
149
|
+
const out = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const part of text.split(/,|\bor\b|\band\b|\//i)) {
|
|
151
|
+
const c = countryOf(part) ?? (/\bpoland\b/i.test(part) ? "Poland" : void 0);
|
|
152
|
+
if (c) out.add(c);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
147
156
|
var RULES = [
|
|
157
|
+
// Screening questions that sound like other rules but are not: answered before the broad rules below see them.
|
|
158
|
+
{ test: /employment agreements?|post[- ]employment restrictions?|non[- ]?compete|non[- ]?solicit|restrictive covenants?/i, answer: (_s, o) => yesNo(o, false) },
|
|
159
|
+
{ test: /(previously|ever|before) (worked|been employed|consulted|interned)( at| for| with)|former (employee|contractor) (of|at)/i, answer: (_s, o) => yesNo(o, false) },
|
|
160
|
+
// "...sponsorship to remain in your current location": about where the person lives, not where the job is.
|
|
161
|
+
{ test: /sponsor[^?]*(current(ly)? (location|country)|where you (currently )?(live|reside|are based))/i, answer: (s, o) => {
|
|
162
|
+
const home = countryOf(s.location);
|
|
163
|
+
if (!home) return void 0;
|
|
164
|
+
const ok = s.workAuthorizedCountries.split(",").some((c) => c.trim().toLowerCase() === home.toLowerCase());
|
|
165
|
+
return yesNo(o, !ok);
|
|
166
|
+
} },
|
|
167
|
+
// "Are you currently located in either Canada, UK or Poland?"
|
|
168
|
+
{ test: /(currently |presently )?(locat(ed|ion)|living|based|residing|reside) in (either )?[A-Z][^?]*\b(or|and)\b/i, answer: (s, o, _c, label) => {
|
|
169
|
+
const home = countryOf(s.location);
|
|
170
|
+
if (!home || !label) return void 0;
|
|
171
|
+
const listed = countriesIn(label.replace(/^.*?\b(?:in|either)\b/i, ""));
|
|
172
|
+
return listed.size ? yesNo(o, listed.has(home)) : void 0;
|
|
173
|
+
} },
|
|
148
174
|
{ test: /^first\s*name/i, answer: (s) => or(s.firstName) },
|
|
149
175
|
{ test: /^last\s*name|surname|family name/i, answer: (s) => or(s.lastName) },
|
|
150
176
|
{ test: /^(full |legal )?name$/i, answer: (s) => or(`${s.firstName} ${s.lastName}`.trim()) },
|
|
@@ -393,18 +419,43 @@ function findEmptyRequired() {
|
|
|
393
419
|
for (const lab of labels) {
|
|
394
420
|
const raw = textOf(lab);
|
|
395
421
|
const ctl = lab.control;
|
|
422
|
+
const opt = ctl;
|
|
423
|
+
if (opt && /radio|checkbox/.test(opt.type || "") && opt.name) {
|
|
424
|
+
if (seen.has(`name:${opt.name}`)) continue;
|
|
425
|
+
seen.add(`name:${opt.name}`);
|
|
426
|
+
const group = Array.from(document.querySelectorAll(`input[name="${CSS.escape(opt.name)}"]`));
|
|
427
|
+
const required2 = group.some((g) => g.required || g.getAttribute("aria-required") === "true");
|
|
428
|
+
if (!required2 || group.some((g) => g.checked)) continue;
|
|
429
|
+
let node = opt.parentElement, question = "";
|
|
430
|
+
while (node && node !== document.body) {
|
|
431
|
+
if (group.every((g) => node.contains(g))) {
|
|
432
|
+
const q = node.querySelector(".application-label, legend, [class*=question-title], [class*=question] > div:first-child, h3, h4");
|
|
433
|
+
question = clean(textOf(q));
|
|
434
|
+
if (question) break;
|
|
435
|
+
}
|
|
436
|
+
node = node.parentElement;
|
|
437
|
+
}
|
|
438
|
+
const label2 = question || clean(raw);
|
|
439
|
+
if (label2 && !seen.has(label2)) {
|
|
440
|
+
seen.add(label2);
|
|
441
|
+
out.push(label2);
|
|
442
|
+
}
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
396
445
|
const box = lab.closest("[data-field-entry-id], fieldset, [class*=field-entry], [class*=question], [class*=field], li") || lab.parentElement;
|
|
397
446
|
if (!box) continue;
|
|
398
447
|
const required = /\*|✱/.test(raw) || /required/i.test(lab.className) || !!(ctl && (ctl.hasAttribute("required") || ctl.getAttribute("aria-required") === "true")) || !!box.querySelector("[required], [aria-required=true]");
|
|
399
448
|
if (!required) continue;
|
|
400
|
-
const
|
|
449
|
+
const combo = !!ctl && (ctl.getAttribute("role") === "combobox" || !!ctl.closest("[class*=select__], [class*=-container]"));
|
|
450
|
+
const scope = ctl && !combo && !/radio|checkbox/.test(ctl.type || "") ? ctl.parentElement || box : box;
|
|
401
451
|
const texts = Array.from(scope.querySelectorAll("input:not([type=hidden]):not([type=radio]):not([type=checkbox]):not([type=file]):not([type=submit]), textarea, select"));
|
|
402
452
|
const files = Array.from(scope.querySelectorAll("input[type=file]"));
|
|
403
453
|
const checks = Array.from(scope.querySelectorAll("input[type=radio], input[type=checkbox]"));
|
|
404
454
|
const buttons = Array.from(scope.querySelectorAll("button, [role=radio], [role=checkbox]")).filter((b) => b.type !== "submit");
|
|
405
455
|
if (!texts.length && !files.length && !checks.length && !buttons.length) continue;
|
|
406
456
|
const scopeText = scope.textContent || "";
|
|
407
|
-
const
|
|
457
|
+
const chosen = !!Array.from(scope.querySelectorAll("[class*=single-value], [class*=multi-value], [class*=singleValue], [class*=multiValue]")).some((e) => (e.textContent || "").trim()) || !!scope.querySelector("[class*=value-container--has-value]") || Array.from(scope.querySelectorAll("input[type=hidden]")).some((h) => h.value.trim() && /question|answer|select|value/i.test(h.name || h.id || ""));
|
|
458
|
+
const filled = chosen || texts.some((t) => t.value?.trim()) || files.some((f) => (f.files?.length || 0) > 0) || files.length > 0 && /\.(pdf|docx?|rtf|txt)\b/i.test(scopeText) || checks.some((c) => c.checked) || buttons.some((b) => b.getAttribute("aria-pressed") === "true" || b.getAttribute("aria-checked") === "true" || /selected|active|checked/i.test(b.className) && !/unselected/i.test(b.className));
|
|
408
459
|
const label = clean(raw).replace(/(ATTACH RESUME|No location found|Couldn't auto-read|Analyzing resume|Loading).*$/i, "").trim();
|
|
409
460
|
if (!filled && label && !seen.has(label)) {
|
|
410
461
|
seen.add(label);
|
|
@@ -1077,6 +1128,12 @@ async function submitGeneric(page) {
|
|
|
1077
1128
|
if (/captcha|robot|verify you are human/i.test(text) || await page.locator("iframe[src*=hcaptcha], iframe[src*=recaptcha], iframe[src*=turnstile]").count()) throw new Error("A captcha is shown. Solve it in the window, press Submit there, then use Mark as submitted here.");
|
|
1078
1129
|
throw new Error("No confirmation text after submit");
|
|
1079
1130
|
}
|
|
1131
|
+
async function dismissCookieBanner(page) {
|
|
1132
|
+
await page.waitForTimeout(800);
|
|
1133
|
+
const button = page.getByRole("button", { name: /^(dismiss|accept( all)?( cookies)?|got it|i agree|agree|ok(ay)?|allow all)$/i }).filter({ visible: true }).first();
|
|
1134
|
+
if (await button.count().catch(() => 0)) await button.click({ timeout: 3e3 }).catch(() => {
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1080
1137
|
async function fill(app) {
|
|
1081
1138
|
const notes = [];
|
|
1082
1139
|
await report(app.id, { status: "filling", notes: ["Runner picked up the application"] });
|
|
@@ -1084,6 +1141,7 @@ async function fill(app) {
|
|
|
1084
1141
|
const page = await b.newPage({ viewport: { width: 1380, height: 940 } });
|
|
1085
1142
|
try {
|
|
1086
1143
|
await page.goto(app.job.applyUrl.replace(/#app$/, ""), { waitUntil: "domcontentloaded" });
|
|
1144
|
+
await dismissCookieBanner(page);
|
|
1087
1145
|
if (app.job.board === "greenhouse") await fillGreenhouse(page, app, notes);
|
|
1088
1146
|
else await discoverAndFillGeneric(page, app, notes);
|
|
1089
1147
|
await page.waitForTimeout(800);
|
|
@@ -1197,8 +1255,16 @@ async function loop() {
|
|
|
1197
1255
|
if (inFlight.has(app.id)) continue;
|
|
1198
1256
|
inFlight.add(app.id);
|
|
1199
1257
|
try {
|
|
1200
|
-
if (app.status === "approved"
|
|
1201
|
-
|
|
1258
|
+
if (app.status === "approved") {
|
|
1259
|
+
const stale = live.get(app.id);
|
|
1260
|
+
if (stale) {
|
|
1261
|
+
await stale.page.close().catch(() => {
|
|
1262
|
+
});
|
|
1263
|
+
live.delete(app.id);
|
|
1264
|
+
log(`refilling ${app.id}: the application changed after it was filled`);
|
|
1265
|
+
}
|
|
1266
|
+
await fill(app);
|
|
1267
|
+
} else if (app.status === "submit_requested") {
|
|
1202
1268
|
await paceSubmit(app.job.applyUrl);
|
|
1203
1269
|
await submit(app);
|
|
1204
1270
|
lastSubmitAt = Date.now();
|
package/package.json
CHANGED