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 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.2" : "dev";
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 scope = ctl && !/radio|checkbox/.test(ctl.type || "") ? ctl.parentElement || box : box;
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 filled = 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));
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" && !live.has(app.id)) await fill(app);
1201
- else if (app.status === "submit_requested") {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-apply",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "The Lazy Apply runner: fills your approved job applications in Chrome on your own computer. Run `npx lazy-apply`.",
5
5
  "type": "module",
6
6
  "bin": { "lazy-apply": "dist/cli.js" },