job-pro 1.0.6 → 1.0.8
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/index.js +149 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -60,7 +60,7 @@ import { createRequire as require_createRequire } from "node:module";
|
|
|
60
60
|
function require_module() {
|
|
61
61
|
return { createRequire: require_createRequire };
|
|
62
62
|
}
|
|
63
|
-
const VERSION = "1.0.
|
|
63
|
+
const VERSION = "1.0.7";
|
|
64
64
|
const COMPANIES = [
|
|
65
65
|
{ key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
|
|
66
66
|
{ key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
|
|
@@ -121,7 +121,9 @@ USAGE
|
|
|
121
121
|
job-pro <company> <verb> [options]
|
|
122
122
|
job-pro list [--compact] list all 50 companies + source family
|
|
123
123
|
job-pro status [--compact] survey profile / sessions / memory / chrome
|
|
124
|
-
job-pro profile init [--force]
|
|
124
|
+
job-pro profile init [--interactive] [--force]
|
|
125
|
+
write ~/.jobpro/profile.json
|
|
126
|
+
--interactive fills it via prompts.
|
|
125
127
|
job-pro profile show print the loaded profile
|
|
126
128
|
job-pro --version
|
|
127
129
|
job-pro help
|
|
@@ -162,6 +164,7 @@ VERBS (same surface for every company)
|
|
|
162
164
|
--print-form emit a fillable JSON template
|
|
163
165
|
--form-file <path> merge per-job answers
|
|
164
166
|
--interactive prompt for unanswered fields
|
|
167
|
+
--batch <file|-> apply to many post_ids (one/line)
|
|
165
168
|
--debug-submit-to <url> verify wire format
|
|
166
169
|
--really-submit actually fire (env-gated)
|
|
167
170
|
memory list | get <k> | set k=v | event <kind> [payload] | clear
|
|
@@ -411,15 +414,94 @@ async function runCompany(adapter, company, rawArgs) {
|
|
|
411
414
|
return emit(adapter.checkResume(text), compact);
|
|
412
415
|
}
|
|
413
416
|
if (verb === "apply") {
|
|
414
|
-
const postId = args[0];
|
|
415
|
-
if (!postId)
|
|
416
|
-
die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path>] [--dry-run | --debug-submit-to <url> | --really-submit]`);
|
|
417
417
|
const reallySubmit = args.includes("--really-submit");
|
|
418
418
|
const printForm = args.includes("--print-form");
|
|
419
419
|
const interactive = args.includes("--interactive");
|
|
420
420
|
const { args: aDebug, value: debugUrl } = popFlagValue(args, "--debug-submit-to");
|
|
421
|
-
const { args:
|
|
422
|
-
|
|
421
|
+
const { args: aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
|
|
422
|
+
const { args: aBatch, value: batchPath } = popFlagValue(aForm, "--batch");
|
|
423
|
+
// Batch mode: read post_ids from a file (or stdin if "-"). Each non-empty,
|
|
424
|
+
// non-`#`-prefixed line is a post_id. Output is a JSON array of
|
|
425
|
+
// { post_id, result } so downstream tooling can iterate.
|
|
426
|
+
if (batchPath) {
|
|
427
|
+
if (reallySubmit) {
|
|
428
|
+
die(`--batch + --really-submit is intentionally refused. Submitting to ` +
|
|
429
|
+
`multiple jobs at once is the exact failure mode this CLI is designed to ` +
|
|
430
|
+
`prevent. Drop --really-submit and use --debug-submit-to <url> for batch ` +
|
|
431
|
+
`verification, or run apply one job at a time.`);
|
|
432
|
+
}
|
|
433
|
+
let rawLines;
|
|
434
|
+
try {
|
|
435
|
+
rawLines = batchPath === "-" ? readFileSync(0, "utf8") : readFileSync(batchPath, "utf8");
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
die(`could not read batch file ${batchPath}: ${err instanceof Error ? err.message : err}`);
|
|
439
|
+
}
|
|
440
|
+
const postIds = rawLines
|
|
441
|
+
.split("\n")
|
|
442
|
+
.map((l) => l.trim())
|
|
443
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
444
|
+
if (postIds.length === 0)
|
|
445
|
+
die(`batch file ${batchPath} contains no post_ids`);
|
|
446
|
+
// We need the schema fetcher / profile / session ONCE, not per-job.
|
|
447
|
+
const fetchSchema = adapter.fetchApplicationSchema;
|
|
448
|
+
if (typeof fetchSchema !== "function") {
|
|
449
|
+
return emit({ ok: false, source: company, message: `apply: not wired for "${company}"` }, compact);
|
|
450
|
+
}
|
|
451
|
+
const prof = loadProfile();
|
|
452
|
+
if (!prof.ok)
|
|
453
|
+
die(prof.message);
|
|
454
|
+
let effectiveProfile = prof.profile;
|
|
455
|
+
if (formFilePath) {
|
|
456
|
+
const merged = applyFormFile(effectiveProfile, formFilePath);
|
|
457
|
+
if (!merged.ok)
|
|
458
|
+
die(merged.message);
|
|
459
|
+
effectiveProfile = merged.profile;
|
|
460
|
+
}
|
|
461
|
+
const session = loadSession(company);
|
|
462
|
+
const out = [];
|
|
463
|
+
for (const id of postIds) {
|
|
464
|
+
try {
|
|
465
|
+
const schemaResult = (await fetchSchema.call(adapter, id));
|
|
466
|
+
if (!schemaResult.ok || !schemaResult.schema) {
|
|
467
|
+
out.push({ post_id: id, ok: false, message: schemaResult.message ?? "schema fetch failed" });
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const staged = stageApplication(schemaResult.schema, effectiveProfile);
|
|
471
|
+
if (debugUrl) {
|
|
472
|
+
const kind = schemaResult.schema.submit_kind ?? "multipart-anon";
|
|
473
|
+
const debugExecutor = kind === "feishu-3-step" ? executeFeishu3Step :
|
|
474
|
+
kind === "moka-aes" ? executeMokaApply :
|
|
475
|
+
kind === "beisen-wecruit" ? executeBeisenWecruit :
|
|
476
|
+
kind === "beisen-italent" ? executeBeisenITalent :
|
|
477
|
+
kind === "cdp-real-browser" ? executeCdpRealBrowser :
|
|
478
|
+
null;
|
|
479
|
+
const result = debugExecutor
|
|
480
|
+
? await debugExecutor(staged, session, { kind: "debug", url: debugUrl })
|
|
481
|
+
: await submitApplication(staged, { kind: "debug", url: debugUrl });
|
|
482
|
+
out.push({ post_id: id, ok: result.ok, ready: staged.ready, submit_kind: kind, debug_result: result });
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
out.push({
|
|
486
|
+
post_id: id,
|
|
487
|
+
ok: staged.ready,
|
|
488
|
+
ready: staged.ready,
|
|
489
|
+
submit_kind: schemaResult.schema.submit_kind,
|
|
490
|
+
message: staged.ready ? "staged ok" : `${staged.unanswered_required.length} required field(s) unfilled`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
out.push({ post_id: id, ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const okCount = out.filter((r) => r.ok).length;
|
|
499
|
+
return emit({ mode: debugUrl ? "batch-debug" : "batch-dry-run", company, total: out.length, ok_count: okCount, results: out }, compact);
|
|
500
|
+
}
|
|
501
|
+
void aBatch;
|
|
502
|
+
const postId = args[0];
|
|
503
|
+
if (!postId)
|
|
504
|
+
die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path> | --interactive | --batch <file>] [--debug-submit-to <url> | --really-submit]`);
|
|
423
505
|
const fetchSchema = adapter.fetchApplicationSchema;
|
|
424
506
|
if (typeof fetchSchema !== "function") {
|
|
425
507
|
return emit({
|
|
@@ -856,6 +938,56 @@ function printStatus(compact) {
|
|
|
856
938
|
console.log(` needed for: lilith adapter, --proxy-server geo-bypass (hikvision).`);
|
|
857
939
|
}
|
|
858
940
|
}
|
|
941
|
+
async function runProfileInitInteractive(template) {
|
|
942
|
+
if (!process.stdin.isTTY) {
|
|
943
|
+
die("profile init --interactive needs a TTY (got a piped stdin). " +
|
|
944
|
+
"Either run from a real terminal, or drop --interactive and edit " +
|
|
945
|
+
"the JSON file directly.");
|
|
946
|
+
}
|
|
947
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
948
|
+
const ask = (prompt) => new Promise((resolve, reject) => {
|
|
949
|
+
let answered = false;
|
|
950
|
+
const onClose = () => {
|
|
951
|
+
if (!answered)
|
|
952
|
+
reject(new Error("stdin closed before answer"));
|
|
953
|
+
};
|
|
954
|
+
rl.once("close", onClose);
|
|
955
|
+
rl.question(prompt, (a) => {
|
|
956
|
+
answered = true;
|
|
957
|
+
rl.off("close", onClose);
|
|
958
|
+
resolve(a);
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
const filled = { ...template };
|
|
962
|
+
console.log(`\nProfile setup — fill in 5 fields (Ctrl-C to abort).\n`);
|
|
963
|
+
try {
|
|
964
|
+
filled.first_name = await prompt("First name: ", ask, (v) => v.trim().length > 0 || "(required)");
|
|
965
|
+
filled.last_name = await prompt("Last name: ", ask, (v) => v.trim().length > 0 || "(required)");
|
|
966
|
+
filled.email = await prompt("Email: ", ask, (v) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? true : "(must look like name@domain.tld)"));
|
|
967
|
+
filled.phone = await prompt("Phone (with country code, e.g. +86 13800138000): ", ask, (v) => (/^[+]?[\d\s\-()]{7,}$/.test(v.trim()) ? true : "(digits + optional spaces/dashes; min 7)"));
|
|
968
|
+
filled.resume_path = await prompt("Resume file path (absolute, PDF/DOCX): ", ask, (v) => {
|
|
969
|
+
const p = v.trim();
|
|
970
|
+
if (!p)
|
|
971
|
+
return "(required — pass an absolute path to your résumé)";
|
|
972
|
+
if (!existsSync(p))
|
|
973
|
+
return `(file not found: ${p})`;
|
|
974
|
+
return true;
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
finally {
|
|
978
|
+
rl.close();
|
|
979
|
+
}
|
|
980
|
+
return filled;
|
|
981
|
+
}
|
|
982
|
+
async function prompt(q, ask, validate) {
|
|
983
|
+
while (true) {
|
|
984
|
+
const v = (await ask(q)).trim();
|
|
985
|
+
const res = validate(v);
|
|
986
|
+
if (res === true)
|
|
987
|
+
return v;
|
|
988
|
+
console.log(` ${res}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
859
991
|
function printCompanyList(compact) {
|
|
860
992
|
// Validate the directory still matches the ADAPTERS map. If a company
|
|
861
993
|
// appears in only one place, treat it as a bug.
|
|
@@ -933,8 +1065,15 @@ async function main() {
|
|
|
933
1065
|
process.exit(1);
|
|
934
1066
|
}
|
|
935
1067
|
mkdirSync(dirname(path), { recursive: true });
|
|
936
|
-
|
|
937
|
-
|
|
1068
|
+
const interactive = args.includes("--interactive");
|
|
1069
|
+
const filled = interactive ? await runProfileInitInteractive(template) : template;
|
|
1070
|
+
writeFileSync(path, JSON.stringify(filled, null, 2) + "\n", "utf8");
|
|
1071
|
+
if (interactive) {
|
|
1072
|
+
console.log(`\nWrote ${path}. Run \`job-pro status\` to confirm, then \`job-pro <co> apply <id>\` to start.`);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
console.log(`Wrote ${path}. Fill in first_name / last_name / email / phone / resume_path before running \`job-pro <co> apply\`. (Tip: pass --interactive to fill it in the terminal now.)`);
|
|
1076
|
+
}
|
|
938
1077
|
return;
|
|
939
1078
|
}
|
|
940
1079
|
if (sub === "show") {
|
|
@@ -946,7 +1085,7 @@ async function main() {
|
|
|
946
1085
|
console.log(JSON.stringify(r.profile, null, 2));
|
|
947
1086
|
return;
|
|
948
1087
|
}
|
|
949
|
-
die(`usage: job-pro profile <init [--force] | show>`);
|
|
1088
|
+
die(`usage: job-pro profile <init [--interactive] [--force] | show>`);
|
|
950
1089
|
}
|
|
951
1090
|
const adapter = ADAPTERS[cmd];
|
|
952
1091
|
if (adapter) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies, all 50 live. 46 via each company's own API; the 4 with no public canonical feed (Hikvision, CICC, Cainiao, WeBank) surfaced via Liepin as a clearly-labeled third-party fallback. No signup, no token, no server.",
|
|
5
5
|
"homepage": "https://job.ha7ch.com",
|
|
6
6
|
"repository": "https://github.com/HA7CH/job-pro",
|