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.
Files changed (2) hide show
  1. package/dist/index.js +149 -10
  2. 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.6";
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] write ~/.jobpro/profile.json template
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: _aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
422
- void _aForm;
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
- writeFileSync(path, JSON.stringify(template, null, 2) + "\n", "utf8");
937
- console.log(`Wrote ${path}. Fill in first_name / last_name / email / phone / resume_path before running \`job-pro <co> apply\`.`);
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.6",
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",