job-pro 1.0.5 → 1.0.7

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 (3) hide show
  1. package/dist/apply.js +132 -163
  2. package/dist/index.js +86 -6
  3. package/package.json +1 -1
package/dist/apply.js CHANGED
@@ -576,33 +576,20 @@ export async function executeFeishu3Step(staged, session, target) {
576
576
  const sessionHeaders = sessionHeaderBag(session, host);
577
577
  // STEP 1 — upload tokens
578
578
  const step1Url = debug ? target.url : `${apiRoot}/attachment/upload/tokens`;
579
- let step1Resp;
580
- try {
581
- step1Resp = await fetch(step1Url, {
582
- method: "POST",
583
- headers: {
584
- ...sessionHeaders,
585
- "Content-Type": "application/json",
586
- Accept: "application/json, text/plain, */*",
587
- },
588
- body: JSON.stringify({ filename, file_size: fileSize }),
589
- });
590
- }
591
- catch (err) {
592
- steps.push({ step: "upload-tokens", url: step1Url, status: 0, ok: false, message: String(err) });
593
- return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
594
- }
595
- const step1Body = await step1Resp.text();
596
- steps.push({
597
- step: "upload-tokens",
598
- url: step1Url,
599
- status: step1Resp.status,
600
- ok: step1Resp.ok,
601
- message: step1Body.slice(0, 200),
602
- });
603
- if (!step1Resp.ok) {
604
- return { ok: false, posted_to: step1Url, status: step1Resp.status, message: "step 1 failed (upload tokens)", steps, response_preview: step1Body.slice(0, 4000) };
579
+ const s1 = await doStep("upload-tokens", step1Url, {
580
+ method: "POST",
581
+ headers: {
582
+ ...sessionHeaders,
583
+ "Content-Type": "application/json",
584
+ Accept: "application/json, text/plain, */*",
585
+ },
586
+ body: JSON.stringify({ filename, file_size: fileSize }),
587
+ }, steps);
588
+ if (!s1.ok) {
589
+ return { ok: false, posted_to: step1Url, status: s1.status, message: `step 1 failed: ${s1.message}`, steps };
605
590
  }
591
+ const step1Resp = s1.response;
592
+ const step1Body = s1.text;
606
593
  // In debug mode, we don't actually have a presigned URL — short-circuit.
607
594
  if (debug) {
608
595
  return {
@@ -640,23 +627,14 @@ export async function executeFeishu3Step(staged, session, target) {
640
627
  ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
641
628
  : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
642
629
  uploadFd.append("file", filePart, filename);
643
- let step2Resp;
644
- try {
645
- step2Resp = await fetch(upload_url, { method: "POST", body: uploadFd });
630
+ const s2 = await doStep("upload-file", upload_url, { method: "POST", body: uploadFd }, steps);
631
+ if (!s2.ok) {
632
+ return { ok: false, posted_to: upload_url, status: s2.status, message: `step 2 failed: ${s2.message}`, steps };
646
633
  }
647
- catch (err) {
648
- steps.push({ step: "upload-file", url: upload_url, status: 0, ok: false, message: String(err) });
649
- return { ok: false, posted_to: upload_url, message: "step 2 network error", steps };
650
- }
651
- steps.push({
652
- step: "upload-file",
653
- url: upload_url,
654
- status: step2Resp.status,
655
- ok: step2Resp.ok,
656
- message: `HTTP ${step2Resp.status}`,
657
- });
658
- if (!step2Resp.ok) {
659
- return { ok: false, posted_to: upload_url, status: step2Resp.status, message: "step 2 failed (upload to CDN)", steps };
634
+ // s2 already pushed to steps via doStep; if upstream returned non-2xx
635
+ // (after retries on 5xx), surface that.
636
+ if (!s2.response.ok) {
637
+ return { ok: false, posted_to: upload_url, status: s2.response.status, message: "step 2 failed (upload to CDN)", steps };
660
638
  }
661
639
  // STEP 3 — resume/apply
662
640
  const applicantInfo = {};
@@ -671,36 +649,24 @@ export async function executeFeishu3Step(staged, session, target) {
671
649
  applicant_info: applicantInfo,
672
650
  };
673
651
  const step3Url = `${apiRoot}/resume/apply`;
674
- let step3Resp;
675
- try {
676
- step3Resp = await fetch(step3Url, {
677
- method: "POST",
678
- headers: {
679
- ...sessionHeaders,
680
- "Content-Type": "application/json",
681
- Accept: "application/json, text/plain, */*",
682
- },
683
- body: JSON.stringify(step3Body),
684
- });
685
- }
686
- catch (err) {
687
- steps.push({ step: "resume-apply", url: step3Url, status: 0, ok: false, message: String(err) });
688
- return { ok: false, posted_to: step3Url, message: "step 3 network error", steps };
652
+ const s3 = await doStep("resume-apply", step3Url, {
653
+ method: "POST",
654
+ headers: {
655
+ ...sessionHeaders,
656
+ "Content-Type": "application/json",
657
+ Accept: "application/json, text/plain, */*",
658
+ },
659
+ body: JSON.stringify(step3Body),
660
+ }, steps);
661
+ if (!s3.ok) {
662
+ return { ok: false, posted_to: step3Url, status: s3.status, message: `step 3 failed: ${s3.message}`, steps };
689
663
  }
690
- const step3Text = await step3Resp.text();
691
- steps.push({
692
- step: "resume-apply",
693
- url: step3Url,
694
- status: step3Resp.status,
695
- ok: step3Resp.ok,
696
- message: step3Text.slice(0, 200),
697
- });
698
664
  return {
699
- ok: step3Resp.ok,
700
- status: step3Resp.status,
665
+ ok: s3.response.ok,
666
+ status: s3.response.status,
701
667
  posted_to: step3Url,
702
- response_preview: step3Text.slice(0, 4000),
703
- message: step3Resp.ok ? "Feishu 3-step submission accepted" : `step 3 rejected: HTTP ${step3Resp.status}`,
668
+ response_preview: s3.text,
669
+ message: s3.response.ok ? "Feishu 3-step submission accepted" : `step 3 rejected: HTTP ${s3.response.status}`,
704
670
  steps,
705
671
  };
706
672
  }
@@ -748,6 +714,42 @@ function retryDelayMs(attempt) {
748
714
  function sleep(ms) {
749
715
  return new Promise((resolve) => setTimeout(resolve, ms));
750
716
  }
717
+ /**
718
+ * Family-executor convenience wrapper. Combines fetchWithRetry's
719
+ * transient-failure handling with the FeishuStepLog bookkeeping that
720
+ * each executor needs to push into result.steps. Returns the response
721
+ * + decoded text, or the error message; either way appends one entry
722
+ * to `steps[]`.
723
+ */
724
+ async function doStep(step, url, init, steps) {
725
+ const r = await fetchWithRetry(url, init, step);
726
+ if (!r.ok) {
727
+ steps.push({
728
+ step,
729
+ url,
730
+ status: r.status ?? 0,
731
+ ok: false,
732
+ message: r.message.slice(0, 200),
733
+ });
734
+ return { ok: false, status: r.status, message: r.message };
735
+ }
736
+ const response = r.response;
737
+ let text = "";
738
+ try {
739
+ text = (await response.text()).slice(0, 4000);
740
+ }
741
+ catch {
742
+ /* binary or stream */
743
+ }
744
+ steps.push({
745
+ step,
746
+ url,
747
+ status: response.status,
748
+ ok: response.ok,
749
+ message: text.slice(0, 200) || `HTTP ${response.status}`,
750
+ });
751
+ return { ok: true, response, text };
752
+ }
751
753
  /** Build the headers bag used by every Feishu/Beisen/Moka step. */
752
754
  function sessionHeaderBag(session, targetHost) {
753
755
  const out = {
@@ -838,42 +840,32 @@ export async function executeMokaApply(staged, session, target) {
838
840
  fd.append("resume", filePart, filename);
839
841
  const steps = [];
840
842
  const sessionHeaders = sessionHeaderBag(session, host);
841
- // Pre-flight limit check (optional — skip in debug since we'd redirect)
843
+ // Pre-flight limit check (optional — skip in debug since we'd redirect).
844
+ // Best-effort; we ignore failures here because the upstream submit will
845
+ // surface any blocker more authoritatively.
842
846
  if (!debug && session) {
843
847
  const lc = `${apiRoot}/api/outer/ats-apply/website/applicant-limit-check`;
844
- try {
845
- const resp = await fetch(lc, {
846
- method: "POST",
847
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
848
- body: JSON.stringify({ orgId: slug, jobId: staged.post_id }),
849
- });
850
- const txt = (await resp.text()).slice(0, 200);
851
- steps.push({ step: "limit-check", url: lc, status: resp.status, ok: resp.ok, message: txt });
852
- }
853
- catch (err) {
854
- steps.push({ step: "limit-check", url: lc, status: 0, ok: false, message: String(err) });
855
- }
856
- }
857
- // Final submit
858
- let resp;
859
- try {
860
- resp = await fetch(targetUrl, {
848
+ await doStep("limit-check", lc, {
861
849
  method: "POST",
862
- headers: sessionHeaders, // Content-Type: multipart/form-data; boundary set by undici
863
- body: fd,
864
- });
865
- }
866
- catch (err) {
867
- steps.push({ step: "apply", url: targetUrl, status: 0, ok: false, message: String(err) });
868
- return { ok: false, posted_to: targetUrl, message: "apply step network error", steps };
850
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
851
+ body: JSON.stringify({ orgId: slug, jobId: staged.post_id }),
852
+ }, steps);
869
853
  }
870
- const text = (await resp.text()).slice(0, 4000);
871
- steps.push({ step: "apply", url: targetUrl, status: resp.status, ok: resp.ok, message: text.slice(0, 200) });
854
+ // Final submit
855
+ const sFinal = await doStep("apply", targetUrl, {
856
+ method: "POST",
857
+ headers: sessionHeaders, // Content-Type: multipart/form-data; boundary set by undici
858
+ body: fd,
859
+ }, steps);
860
+ if (!sFinal.ok) {
861
+ return { ok: false, posted_to: targetUrl, status: sFinal.status, message: `apply failed: ${sFinal.message}`, steps };
862
+ }
863
+ const resp = sFinal.response;
872
864
  return {
873
865
  ok: resp.ok,
874
866
  status: resp.status,
875
867
  posted_to: targetUrl,
876
- response_preview: text,
868
+ response_preview: sFinal.text,
877
869
  message: resp.ok ? "Moka apply submitted" : `Moka apply rejected: HTTP ${resp.status}`,
878
870
  steps,
879
871
  };
@@ -933,16 +925,12 @@ export async function executeBeisenWecruit(staged, session, target) {
933
925
  ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
934
926
  : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
935
927
  uploadFd.append("file", filePart, filename);
936
- let r1;
937
- try {
938
- r1 = await fetch(step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd });
939
- }
940
- catch (err) {
941
- steps.push({ step: "upload-file", url: step1Url, status: 0, ok: false, message: String(err) });
942
- return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
928
+ const s1 = await doStep("upload-file", step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd }, steps);
929
+ if (!s1.ok) {
930
+ return { ok: false, posted_to: step1Url, status: s1.status, message: `step 1 failed: ${s1.message}`, steps };
943
931
  }
944
- const text1 = (await r1.text()).slice(0, 2000);
945
- steps.push({ step: "upload-file", url: step1Url, status: r1.status, ok: r1.ok, message: text1.slice(0, 200) });
932
+ const r1 = s1.response;
933
+ const text1 = s1.text;
946
934
  if (debug) {
947
935
  return { ok: r1.ok, status: r1.status, posted_to: step1Url, message: "debug: step 1 fired, steps 2+3 skipped", steps, response_preview: text1 };
948
936
  }
@@ -957,36 +945,26 @@ export async function executeBeisenWecruit(staged, session, target) {
957
945
  catch { /* keep empty */ }
958
946
  // STEP 2 — profile info
959
947
  const step2Url = `${apiBase}/resume/info/add/${encodeURIComponent(su)}`;
960
- let r2;
961
- try {
962
- r2 = await fetch(step2Url, {
963
- method: "POST",
964
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
965
- body: JSON.stringify({ name: applicant.name, email: applicant.email, phone: applicant.phone, attachmentId }),
966
- });
948
+ const s2 = await doStep("profile-add", step2Url, {
949
+ method: "POST",
950
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
951
+ body: JSON.stringify({ name: applicant.name, email: applicant.email, phone: applicant.phone, attachmentId }),
952
+ }, steps);
953
+ if (!s2.ok) {
954
+ return { ok: false, posted_to: step2Url, status: s2.status, message: `step 2 failed: ${s2.message}`, steps };
967
955
  }
968
- catch (err) {
969
- steps.push({ step: "profile-add", url: step2Url, status: 0, ok: false, message: String(err) });
970
- return { ok: false, posted_to: step2Url, message: "step 2 network error", steps };
971
- }
972
- const text2 = (await r2.text()).slice(0, 2000);
973
- steps.push({ step: "profile-add", url: step2Url, status: r2.status, ok: r2.ok, message: text2.slice(0, 200) });
974
956
  // STEP 3 — final delivery
975
957
  const step3Url = `${apiBase}/delivery/resume/${encodeURIComponent(su)}`;
976
- let r3;
977
- try {
978
- r3 = await fetch(step3Url, {
979
- method: "POST",
980
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
981
- body: JSON.stringify({ postId: staged.post_id, attachmentId }),
982
- });
983
- }
984
- catch (err) {
985
- steps.push({ step: "deliver", url: step3Url, status: 0, ok: false, message: String(err) });
986
- return { ok: false, posted_to: step3Url, message: "step 3 network error", steps };
987
- }
988
- const text3 = (await r3.text()).slice(0, 4000);
989
- steps.push({ step: "deliver", url: step3Url, status: r3.status, ok: r3.ok, message: text3.slice(0, 200) });
958
+ const s3 = await doStep("deliver", step3Url, {
959
+ method: "POST",
960
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
961
+ body: JSON.stringify({ postId: staged.post_id, attachmentId }),
962
+ }, steps);
963
+ if (!s3.ok) {
964
+ return { ok: false, posted_to: step3Url, status: s3.status, message: `step 3 failed: ${s3.message}`, steps };
965
+ }
966
+ const r3 = s3.response;
967
+ const text3 = s3.text;
990
968
  return {
991
969
  ok: r3.ok,
992
970
  status: r3.status,
@@ -1050,16 +1028,12 @@ export async function executeBeisenITalent(staged, session, target) {
1050
1028
  ? new FileCtor([new Uint8Array(resumeBytes)], filename, { type: "application/pdf" })
1051
1029
  : new Blob([new Uint8Array(resumeBytes)], { type: "application/pdf" });
1052
1030
  uploadFd.append("file", filePart, filename);
1053
- let r1;
1054
- try {
1055
- r1 = await fetch(step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd });
1031
+ const s1 = await doStep("upload", step1Url, { method: "POST", headers: sessionHeaders, body: uploadFd }, steps);
1032
+ if (!s1.ok) {
1033
+ return { ok: false, posted_to: step1Url, status: s1.status, message: `step 1 failed: ${s1.message}`, steps };
1056
1034
  }
1057
- catch (err) {
1058
- steps.push({ step: "upload", url: step1Url, status: 0, ok: false, message: String(err) });
1059
- return { ok: false, posted_to: step1Url, message: "step 1 network error", steps };
1060
- }
1061
- const text1 = (await r1.text()).slice(0, 2000);
1062
- steps.push({ step: "upload", url: step1Url, status: r1.status, ok: r1.ok, message: text1.slice(0, 200) });
1035
+ const r1 = s1.response;
1036
+ const text1 = s1.text;
1063
1037
  if (debug) {
1064
1038
  return { ok: r1.ok, status: r1.status, posted_to: step1Url, message: "debug: step 1 fired, step 2 skipped", steps, response_preview: text1 };
1065
1039
  }
@@ -1073,31 +1047,26 @@ export async function executeBeisenITalent(staged, session, target) {
1073
1047
  catch { /* keep empty */ }
1074
1048
  // STEP 2 — submit apply
1075
1049
  const step2Url = `${apiRoot}/api/Apply/SubmitResume`;
1076
- let r2;
1077
- try {
1078
- r2 = await fetch(step2Url, {
1079
- method: "POST",
1080
- headers: { ...sessionHeaders, "Content-Type": "application/json" },
1081
- body: JSON.stringify({
1082
- JobAdId: staged.post_id,
1083
- ResumeId: resumeId,
1084
- Name: applicant.name,
1085
- Email: applicant.email,
1086
- Mobile: applicant.phone,
1087
- }),
1088
- });
1089
- }
1090
- catch (err) {
1091
- steps.push({ step: "submit", url: step2Url, status: 0, ok: false, message: String(err) });
1092
- return { ok: false, posted_to: step2Url, message: "step 2 network error", steps };
1093
- }
1094
- const text2 = (await r2.text()).slice(0, 4000);
1095
- steps.push({ step: "submit", url: step2Url, status: r2.status, ok: r2.ok, message: text2.slice(0, 200) });
1050
+ const s2 = await doStep("submit", step2Url, {
1051
+ method: "POST",
1052
+ headers: { ...sessionHeaders, "Content-Type": "application/json" },
1053
+ body: JSON.stringify({
1054
+ JobAdId: staged.post_id,
1055
+ ResumeId: resumeId,
1056
+ Name: applicant.name,
1057
+ Email: applicant.email,
1058
+ Mobile: applicant.phone,
1059
+ }),
1060
+ }, steps);
1061
+ if (!s2.ok) {
1062
+ return { ok: false, posted_to: step2Url, status: s2.status, message: `step 2 failed: ${s2.message}`, steps };
1063
+ }
1064
+ const r2 = s2.response;
1096
1065
  return {
1097
1066
  ok: r2.ok,
1098
1067
  status: r2.status,
1099
1068
  posted_to: step2Url,
1100
- response_preview: text2,
1069
+ response_preview: s2.text,
1101
1070
  message: r2.ok ? "Beisen iTalent submission accepted" : `step 2 rejected: HTTP ${r2.status}`,
1102
1071
  steps,
1103
1072
  };
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.5";
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 / 字节跳动" },
@@ -162,6 +162,7 @@ VERBS (same surface for every company)
162
162
  --print-form emit a fillable JSON template
163
163
  --form-file <path> merge per-job answers
164
164
  --interactive prompt for unanswered fields
165
+ --batch <file|-> apply to many post_ids (one/line)
165
166
  --debug-submit-to <url> verify wire format
166
167
  --really-submit actually fire (env-gated)
167
168
  memory list | get <k> | set k=v | event <kind> [payload] | clear
@@ -411,15 +412,94 @@ async function runCompany(adapter, company, rawArgs) {
411
412
  return emit(adapter.checkResume(text), compact);
412
413
  }
413
414
  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
415
  const reallySubmit = args.includes("--really-submit");
418
416
  const printForm = args.includes("--print-form");
419
417
  const interactive = args.includes("--interactive");
420
418
  const { args: aDebug, value: debugUrl } = popFlagValue(args, "--debug-submit-to");
421
- const { args: _aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
422
- void _aForm;
419
+ const { args: aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
420
+ const { args: aBatch, value: batchPath } = popFlagValue(aForm, "--batch");
421
+ // Batch mode: read post_ids from a file (or stdin if "-"). Each non-empty,
422
+ // non-`#`-prefixed line is a post_id. Output is a JSON array of
423
+ // { post_id, result } so downstream tooling can iterate.
424
+ if (batchPath) {
425
+ if (reallySubmit) {
426
+ die(`--batch + --really-submit is intentionally refused. Submitting to ` +
427
+ `multiple jobs at once is the exact failure mode this CLI is designed to ` +
428
+ `prevent. Drop --really-submit and use --debug-submit-to <url> for batch ` +
429
+ `verification, or run apply one job at a time.`);
430
+ }
431
+ let rawLines;
432
+ try {
433
+ rawLines = batchPath === "-" ? readFileSync(0, "utf8") : readFileSync(batchPath, "utf8");
434
+ }
435
+ catch (err) {
436
+ die(`could not read batch file ${batchPath}: ${err instanceof Error ? err.message : err}`);
437
+ }
438
+ const postIds = rawLines
439
+ .split("\n")
440
+ .map((l) => l.trim())
441
+ .filter((l) => l && !l.startsWith("#"));
442
+ if (postIds.length === 0)
443
+ die(`batch file ${batchPath} contains no post_ids`);
444
+ // We need the schema fetcher / profile / session ONCE, not per-job.
445
+ const fetchSchema = adapter.fetchApplicationSchema;
446
+ if (typeof fetchSchema !== "function") {
447
+ return emit({ ok: false, source: company, message: `apply: not wired for "${company}"` }, compact);
448
+ }
449
+ const prof = loadProfile();
450
+ if (!prof.ok)
451
+ die(prof.message);
452
+ let effectiveProfile = prof.profile;
453
+ if (formFilePath) {
454
+ const merged = applyFormFile(effectiveProfile, formFilePath);
455
+ if (!merged.ok)
456
+ die(merged.message);
457
+ effectiveProfile = merged.profile;
458
+ }
459
+ const session = loadSession(company);
460
+ const out = [];
461
+ for (const id of postIds) {
462
+ try {
463
+ const schemaResult = (await fetchSchema.call(adapter, id));
464
+ if (!schemaResult.ok || !schemaResult.schema) {
465
+ out.push({ post_id: id, ok: false, message: schemaResult.message ?? "schema fetch failed" });
466
+ continue;
467
+ }
468
+ const staged = stageApplication(schemaResult.schema, effectiveProfile);
469
+ if (debugUrl) {
470
+ const kind = schemaResult.schema.submit_kind ?? "multipart-anon";
471
+ const debugExecutor = kind === "feishu-3-step" ? executeFeishu3Step :
472
+ kind === "moka-aes" ? executeMokaApply :
473
+ kind === "beisen-wecruit" ? executeBeisenWecruit :
474
+ kind === "beisen-italent" ? executeBeisenITalent :
475
+ kind === "cdp-real-browser" ? executeCdpRealBrowser :
476
+ null;
477
+ const result = debugExecutor
478
+ ? await debugExecutor(staged, session, { kind: "debug", url: debugUrl })
479
+ : await submitApplication(staged, { kind: "debug", url: debugUrl });
480
+ out.push({ post_id: id, ok: result.ok, ready: staged.ready, submit_kind: kind, debug_result: result });
481
+ }
482
+ else {
483
+ out.push({
484
+ post_id: id,
485
+ ok: staged.ready,
486
+ ready: staged.ready,
487
+ submit_kind: schemaResult.schema.submit_kind,
488
+ message: staged.ready ? "staged ok" : `${staged.unanswered_required.length} required field(s) unfilled`,
489
+ });
490
+ }
491
+ }
492
+ catch (err) {
493
+ out.push({ post_id: id, ok: false, message: err instanceof Error ? err.message : String(err) });
494
+ }
495
+ }
496
+ const okCount = out.filter((r) => r.ok).length;
497
+ return emit({ mode: debugUrl ? "batch-debug" : "batch-dry-run", company, total: out.length, ok_count: okCount, results: out }, compact);
498
+ }
499
+ void aBatch;
500
+ const postId = args[0];
501
+ if (!postId)
502
+ die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path> | --interactive | --batch <file>] [--debug-submit-to <url> | --really-submit]`);
423
503
  const fetchSchema = adapter.fetchApplicationSchema;
424
504
  if (typeof fetchSchema !== "function") {
425
505
  return emit({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",