lazy-apply 0.0.0-stage → 0.1.1
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/README.md +27 -2
- package/dist/cli.js +311 -0
- package/dist/runner.js +1170 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lazy-apply
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The runner for [Lazy Apply](https://lazyapply.online). It fills the job applications you approve in
|
|
4
|
+
Lazy Apply, in a Chrome window on your own computer, and waits for you to press Submit.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npx lazy-apply
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
That one command:
|
|
11
|
+
|
|
12
|
+
1. opens your browser so you can connect this computer to your Lazy Apply account (no token to copy);
|
|
13
|
+
2. checks Google Chrome is installed;
|
|
14
|
+
3. offers to start automatically in the background whenever you log in, keeping itself up to date.
|
|
15
|
+
|
|
16
|
+
Needs Node.js 18 or newer and Google Chrome. Works on macOS, Windows and Linux.
|
|
17
|
+
|
|
18
|
+
| Command | What it does |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `npx lazy-apply` | Set up (once) and start |
|
|
21
|
+
| `npx lazy-apply status` | Account, automatic start, whether it is running |
|
|
22
|
+
| `npx lazy-apply autostart on` / `off` | Start, or stop starting, automatically at login |
|
|
23
|
+
| `npx lazy-apply start` | Run now, in this terminal |
|
|
24
|
+
| `npx lazy-apply logout` | Disconnect this computer |
|
|
25
|
+
|
|
26
|
+
Settings are in `~/.lazy-apply/config.json` and the log in `~/.lazy-apply/runner.log`. Forms are
|
|
27
|
+
filled in a separate Chrome profile (`~/.auto-apply/chrome-profile`), never your everyday one. Nothing is
|
|
28
|
+
ever submitted without your Submit in Lazy Apply.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/cli/src/cli.ts
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
var VERSION = true ? "0.1.1" : "dev";
|
|
10
|
+
var DEFAULT_APP = "https://lazyapply.online";
|
|
11
|
+
var HOME = join(homedir(), ".lazy-apply");
|
|
12
|
+
var CONFIG = join(HOME, "config.json");
|
|
13
|
+
var LOG = join(HOME, "runner.log");
|
|
14
|
+
var LABEL = "online.lazyapply.runner";
|
|
15
|
+
var tty = process.stdout.isTTY;
|
|
16
|
+
var paint = (code) => (s) => tty ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
17
|
+
var bold = paint("1");
|
|
18
|
+
var dim = paint("2");
|
|
19
|
+
var teal = paint("36");
|
|
20
|
+
var green = paint("32");
|
|
21
|
+
var red = paint("31");
|
|
22
|
+
var yellow = paint("33");
|
|
23
|
+
var say = (s = "") => console.log(s);
|
|
24
|
+
var fail = (s) => {
|
|
25
|
+
console.error(`${red("\u2717")} ${s}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
};
|
|
28
|
+
function load() {
|
|
29
|
+
const fromFlag = flag("--app-url");
|
|
30
|
+
try {
|
|
31
|
+
const c = JSON.parse(readFileSync(CONFIG, "utf8"));
|
|
32
|
+
return fromFlag && fromFlag !== c.appUrl ? { appUrl: fromFlag } : c;
|
|
33
|
+
} catch {
|
|
34
|
+
return { appUrl: fromFlag || process.env.LAZY_APPLY_URL || DEFAULT_APP };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function save(c) {
|
|
38
|
+
mkdirSync(HOME, { recursive: true });
|
|
39
|
+
writeFileSync(CONFIG, JSON.stringify(c, null, 2), { mode: 384 });
|
|
40
|
+
}
|
|
41
|
+
function flag(name) {
|
|
42
|
+
const i = process.argv.indexOf(name);
|
|
43
|
+
return i > 0 ? process.argv[i + 1] : void 0;
|
|
44
|
+
}
|
|
45
|
+
function openBrowser(url) {
|
|
46
|
+
if (process.env.LAZY_APPLY_NO_BROWSER) return;
|
|
47
|
+
const [cmd, args] = platform() === "darwin" ? ["open", [url]] : platform() === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
48
|
+
try {
|
|
49
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function connect(c) {
|
|
54
|
+
const device = `${userInfo().username}@${hostname().replace(/\.local$/, "")}`;
|
|
55
|
+
let start;
|
|
56
|
+
try {
|
|
57
|
+
const r = await fetch(`${c.appUrl}/api/runner/link`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device }) });
|
|
58
|
+
if (!r.ok) throw new Error(`${r.status}`);
|
|
59
|
+
start = await r.json();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return fail(`Could not reach ${c.appUrl} (${e.message}). Check your internet connection and try again.`);
|
|
62
|
+
}
|
|
63
|
+
say(`${bold("Connect this computer to your Lazy Apply account")}`);
|
|
64
|
+
say(`Your browser is opening. Check it shows this code, then press ${bold("Connect this computer")}:`);
|
|
65
|
+
say();
|
|
66
|
+
say(` ${bold(teal(start.code))}`);
|
|
67
|
+
say();
|
|
68
|
+
say(dim(`If it did not open, go to ${start.url}`));
|
|
69
|
+
openBrowser(start.url);
|
|
70
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (Date.now() < start.expiresAt) {
|
|
73
|
+
if (tty) process.stdout.write(`\r${teal(frames[i++ % frames.length])} Waiting for you to confirm in the browser\u2026`);
|
|
74
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
75
|
+
try {
|
|
76
|
+
const r = await fetch(`${c.appUrl}/api/runner/link?secret=${start.secret}`);
|
|
77
|
+
const d = await r.json();
|
|
78
|
+
if (d.status === "approved" && d.token) {
|
|
79
|
+
if (tty) process.stdout.write("\r\x1B[2K");
|
|
80
|
+
const next = { ...c, token: d.token, account: d.account };
|
|
81
|
+
save(next);
|
|
82
|
+
say(`${green("\u2713")} Connected${d.account ? ` as ${bold(d.account)}` : ""}.`);
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
if (d.status === "expired") break;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (tty) process.stdout.write("\r\x1B[2K");
|
|
90
|
+
return fail("The code expired before it was confirmed. Run npx lazy-apply again for a new one.");
|
|
91
|
+
}
|
|
92
|
+
function chromeInstalled() {
|
|
93
|
+
const p = platform();
|
|
94
|
+
const candidates = p === "darwin" ? ["/Applications/Google Chrome.app", join(homedir(), "Applications/Google Chrome.app")] : p === "win32" ? [process.env["PROGRAMFILES"], process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean).map((d) => join(d, "Google/Chrome/Application/chrome.exe")) : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/opt/google/chrome/chrome"];
|
|
95
|
+
return candidates.some((c) => existsSync(c));
|
|
96
|
+
}
|
|
97
|
+
function npxPath() {
|
|
98
|
+
if (platform() === "win32") return join(dirname(process.execPath), "npx.cmd");
|
|
99
|
+
const stable = ["/opt/homebrew/bin/npx", "/usr/local/bin/npx", "/usr/bin/npx"].find((p) => existsSync(p));
|
|
100
|
+
return stable ?? join(dirname(process.execPath), "npx");
|
|
101
|
+
}
|
|
102
|
+
var RUN_ARGS = ["-y", "lazy-apply@latest", "start"];
|
|
103
|
+
function plistPath() {
|
|
104
|
+
return join(homedir(), "Library/LaunchAgents", `${LABEL}.plist`);
|
|
105
|
+
}
|
|
106
|
+
function unitPath() {
|
|
107
|
+
return join(homedir(), ".config/systemd/user/lazy-apply.service");
|
|
108
|
+
}
|
|
109
|
+
function startupPath() {
|
|
110
|
+
return join(process.env.APPDATA || "", "Microsoft/Windows/Start Menu/Programs/Startup/lazy-apply.cmd");
|
|
111
|
+
}
|
|
112
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
113
|
+
function launchAgentPlist(npx, path, log) {
|
|
114
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
115
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
116
|
+
<plist version="1.0">
|
|
117
|
+
<dict>
|
|
118
|
+
<key>Label</key><string>${LABEL}</string>
|
|
119
|
+
<key>ProgramArguments</key>
|
|
120
|
+
<array>${[npx, ...RUN_ARGS].map((a) => `<string>${xml(a)}</string>`).join("")}</array>
|
|
121
|
+
<key>EnvironmentVariables</key>
|
|
122
|
+
<dict><key>PATH</key><string>${xml(path)}</string></dict>
|
|
123
|
+
<key>RunAtLoad</key><true/>
|
|
124
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
125
|
+
<key>ThrottleInterval</key><integer>30</integer>
|
|
126
|
+
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
127
|
+
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
128
|
+
</dict>
|
|
129
|
+
</plist>
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
function enableAutostart() {
|
|
133
|
+
const p = platform();
|
|
134
|
+
const path = `${dirname(npxPath())}:/opt/homebrew/bin:/usr/local/bin:${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
135
|
+
mkdirSync(HOME, { recursive: true });
|
|
136
|
+
if (p === "darwin") {
|
|
137
|
+
mkdirSync(dirname(plistPath()), { recursive: true });
|
|
138
|
+
writeFileSync(plistPath(), launchAgentPlist(npxPath(), path, LOG));
|
|
139
|
+
const uid = process.getuid?.() ?? 501;
|
|
140
|
+
try {
|
|
141
|
+
execFileSync("launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" });
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
execFileSync("launchctl", ["bootstrap", `gui/${uid}`, plistPath()], { stdio: "ignore" });
|
|
145
|
+
return "macOS login item";
|
|
146
|
+
}
|
|
147
|
+
if (p === "linux") {
|
|
148
|
+
mkdirSync(dirname(unitPath()), { recursive: true });
|
|
149
|
+
writeFileSync(unitPath(), `[Unit]
|
|
150
|
+
Description=Lazy Apply runner
|
|
151
|
+
After=graphical-session.target network-online.target
|
|
152
|
+
|
|
153
|
+
[Service]
|
|
154
|
+
ExecStart=${npxPath()} ${RUN_ARGS.join(" ")}
|
|
155
|
+
Environment=PATH=${path}
|
|
156
|
+
${process.env.DISPLAY ? `Environment=DISPLAY=${process.env.DISPLAY}
|
|
157
|
+
` : ""}${process.env.WAYLAND_DISPLAY ? `Environment=WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}
|
|
158
|
+
` : ""}Restart=on-failure
|
|
159
|
+
RestartSec=30
|
|
160
|
+
StandardOutput=append:${LOG}
|
|
161
|
+
StandardError=append:${LOG}
|
|
162
|
+
|
|
163
|
+
[Install]
|
|
164
|
+
WantedBy=default.target
|
|
165
|
+
`);
|
|
166
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
167
|
+
execFileSync("systemctl", ["--user", "enable", "--now", "lazy-apply.service"], { stdio: "ignore" });
|
|
168
|
+
return "systemd user service";
|
|
169
|
+
}
|
|
170
|
+
if (p === "win32") {
|
|
171
|
+
writeFileSync(startupPath(), `@echo off\r
|
|
172
|
+
start "Lazy Apply" /min "${npxPath()}" ${RUN_ARGS.join(" ")}\r
|
|
173
|
+
`);
|
|
174
|
+
spawn("cmd", ["/c", startupPath()], { detached: true, stdio: "ignore" }).unref();
|
|
175
|
+
return "Windows startup item";
|
|
176
|
+
}
|
|
177
|
+
return fail(`Automatic start is not supported on ${p}. Run npx lazy-apply start yourself instead.`);
|
|
178
|
+
}
|
|
179
|
+
function disableAutostart() {
|
|
180
|
+
const p = platform();
|
|
181
|
+
if (p === "darwin") {
|
|
182
|
+
try {
|
|
183
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LABEL}`], { stdio: "ignore" });
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
rmSync(plistPath(), { force: true });
|
|
187
|
+
} else if (p === "linux") {
|
|
188
|
+
try {
|
|
189
|
+
execFileSync("systemctl", ["--user", "disable", "--now", "lazy-apply.service"], { stdio: "ignore" });
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
rmSync(unitPath(), { force: true });
|
|
193
|
+
} else if (p === "win32") rmSync(startupPath(), { force: true });
|
|
194
|
+
}
|
|
195
|
+
var autostartInstalled = () => existsSync(platform() === "darwin" ? plistPath() : platform() === "linux" ? unitPath() : startupPath());
|
|
196
|
+
function runningPid(appUrl) {
|
|
197
|
+
const lock = join(tmpdir(), `auto-apply-runner-chrome-profile-${appUrl.replace(/[^a-z0-9]+/gi, "_")}.lock`);
|
|
198
|
+
try {
|
|
199
|
+
const pid = Number(readFileSync(lock, "utf8"));
|
|
200
|
+
process.kill(pid, 0);
|
|
201
|
+
return pid;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function run(c) {
|
|
207
|
+
if (!c.token) fail("This computer is not connected yet. Run npx lazy-apply first.");
|
|
208
|
+
process.env.APP_URL = c.appUrl;
|
|
209
|
+
process.env.RUNNER_TOKEN = c.token;
|
|
210
|
+
process.env.RUNNER_PROFILE ||= "chrome-profile";
|
|
211
|
+
process.env.RUNNER_CLIENT = `lazy-apply-cli/${VERSION} ${platform()}`;
|
|
212
|
+
say(`${teal("\u25CF")} Lazy Apply runner ${dim(`v${VERSION}`)}${c.account ? ` for ${bold(c.account)}` : ""}. Approved applications fill in a Chrome window. ${dim("Ctrl+C to stop.")}`);
|
|
213
|
+
await import("./runner.js");
|
|
214
|
+
}
|
|
215
|
+
async function ask(question, yes = true) {
|
|
216
|
+
if (!process.stdin.isTTY) return false;
|
|
217
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
218
|
+
const a = (await rl.question(`${question} ${dim(yes ? "[Y/n]" : "[y/N]")} `)).trim().toLowerCase();
|
|
219
|
+
rl.close();
|
|
220
|
+
return a ? a.startsWith("y") : yes;
|
|
221
|
+
}
|
|
222
|
+
async function main() {
|
|
223
|
+
const cmd = process.argv.slice(2).find((a) => !a.startsWith("--") && a !== flag("--app-url")) || "setup";
|
|
224
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
225
|
+
if (major < 18) fail(`Node.js 18 or newer is needed (you have ${process.versions.node}). Install it from https://nodejs.org and try again.`);
|
|
226
|
+
if (cmd === "help" || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
227
|
+
say(`${bold("lazy-apply")} ${dim(`v${VERSION}`)}: fills your approved Lazy Apply applications on this computer.
|
|
228
|
+
`);
|
|
229
|
+
say(` npx lazy-apply set up (once) and start`);
|
|
230
|
+
say(` npx lazy-apply status what it is doing`);
|
|
231
|
+
say(` npx lazy-apply autostart on start automatically when you log in`);
|
|
232
|
+
say(` npx lazy-apply autostart off stop starting automatically`);
|
|
233
|
+
say(` npx lazy-apply start run now, in this terminal`);
|
|
234
|
+
say(` npx lazy-apply logout disconnect this computer`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (cmd === "version" || process.argv.includes("--version")) return say(VERSION);
|
|
238
|
+
let c = load();
|
|
239
|
+
if (cmd === "start") return run(c);
|
|
240
|
+
if (cmd === "status") {
|
|
241
|
+
const pid = runningPid(c.appUrl);
|
|
242
|
+
say(`${bold("Lazy Apply runner")} ${dim(`v${VERSION}`)}`);
|
|
243
|
+
say(` Account ${c.token ? c.account || "connected" : yellow("not connected (run npx lazy-apply)")}`);
|
|
244
|
+
say(` Server ${c.appUrl}`);
|
|
245
|
+
say(` Automatic start ${autostartInstalled() ? green("on") : "off"}`);
|
|
246
|
+
say(` Running ${pid ? green(`yes (pid ${pid})`) : "no"}`);
|
|
247
|
+
say(` Log ${LOG}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (cmd === "autostart") {
|
|
251
|
+
const on = (process.argv[process.argv.indexOf("autostart") + 1] || "on") !== "off";
|
|
252
|
+
if (on) {
|
|
253
|
+
if (!c.token) c = await connect(c);
|
|
254
|
+
const how = enableAutostart();
|
|
255
|
+
save({ ...c, autostart: true });
|
|
256
|
+
say(`${green("\u2713")} Automatic start is on (${how}). It runs in the background whenever you log in.`);
|
|
257
|
+
} else {
|
|
258
|
+
disableAutostart();
|
|
259
|
+
save({ ...c, autostart: false });
|
|
260
|
+
say(`${green("\u2713")} Automatic start is off.`);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (cmd === "logout") {
|
|
265
|
+
disableAutostart();
|
|
266
|
+
save({ appUrl: c.appUrl, autostart: false });
|
|
267
|
+
say(`${green("\u2713")} This computer is disconnected and will not start automatically. Run npx lazy-apply to connect again.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (cmd !== "setup") fail(`Unknown command "${cmd}". Try npx lazy-apply help.`);
|
|
271
|
+
say(`${bold(teal("Lazy Apply"))} ${dim(`runner v${VERSION}`)}`);
|
|
272
|
+
say();
|
|
273
|
+
if (!c.token) {
|
|
274
|
+
c = await connect(c);
|
|
275
|
+
say();
|
|
276
|
+
} else say(`${green("\u2713")} Connected${c.account ? ` as ${bold(c.account)}` : ""}.`);
|
|
277
|
+
if (!chromeInstalled()) {
|
|
278
|
+
say(`${yellow("!")} Google Chrome was not found. The runner fills forms in Chrome, so install it from ${bold("https://www.google.com/chrome/")} and run ${bold("npx lazy-apply")} again.`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
say(`${green("\u2713")} Google Chrome found.`);
|
|
282
|
+
const canAutostart = platform() !== "linux" || !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
|
|
283
|
+
if (c.autostart === void 0 && canAutostart) {
|
|
284
|
+
const yes = await ask(`Start automatically in the background whenever you log in? It also keeps itself up to date.`);
|
|
285
|
+
if (yes) {
|
|
286
|
+
const how = enableAutostart();
|
|
287
|
+
save({ ...c, autostart: true });
|
|
288
|
+
say(`${green("\u2713")} Done (${how}). The runner is now running in the background.`);
|
|
289
|
+
say(dim(` It opens a Chrome window only when an approved application needs filling.`));
|
|
290
|
+
say(dim(` Check on it any time with npx lazy-apply status. Logs: ${LOG}`));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
save({ ...c, autostart: false });
|
|
294
|
+
say(dim(` No problem. Turn it on later with npx lazy-apply autostart on.`));
|
|
295
|
+
} else if (c.autostart && autostartInstalled()) {
|
|
296
|
+
const pid = runningPid(c.appUrl);
|
|
297
|
+
if (pid) {
|
|
298
|
+
say(`${green("\u2713")} Already running in the background (pid ${pid}). Nothing else to do.`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
enableAutostart();
|
|
302
|
+
say(`${green("\u2713")} Started it in the background again.`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
say();
|
|
306
|
+
await run(c);
|
|
307
|
+
}
|
|
308
|
+
main().catch((e) => fail(e.message));
|
|
309
|
+
export {
|
|
310
|
+
launchAgentPlist
|
|
311
|
+
};
|