job-pro 1.0.14 → 1.0.16

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/apply.js CHANGED
@@ -59,6 +59,23 @@ const TEMPLATE = {
59
59
  // nationality: "China",
60
60
  },
61
61
  };
62
+ /**
63
+ * Read profile.json as-is, returning whatever is there.
64
+ * Skips the loadProfile() validation so callers (like `profile lint`)
65
+ * can inspect partial / broken profiles instead of getting a flat fail.
66
+ */
67
+ export function loadProfileRaw() {
68
+ if (!existsSync(PROFILE_PATH)) {
69
+ return { ok: false, path: PROFILE_PATH, message: `profile not found at ${PROFILE_PATH}` };
70
+ }
71
+ try {
72
+ const raw = readFileSync(PROFILE_PATH, "utf8");
73
+ return { ok: true, path: PROFILE_PATH, profile: JSON.parse(raw) };
74
+ }
75
+ catch (err) {
76
+ return { ok: false, path: PROFILE_PATH, message: `could not parse ${PROFILE_PATH}: ${err instanceof Error ? err.message : err}` };
77
+ }
78
+ }
62
79
  export function loadProfile() {
63
80
  if (!existsSync(PROFILE_PATH)) {
64
81
  return {
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ import * as geely from "./geely.js";
50
50
  import * as webank from "./webank.js";
51
51
  import * as horizonrobotics from "./horizonrobotics.js";
52
52
  import * as cambricon from "./cambricon.js";
53
- import { loadProfile, loadSession, profileTemplate, saveProfile, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, buildFormTemplate, applyFormFile, promptUnansweredFields, formatStaged, } from "./apply.js";
53
+ import { loadProfile, loadProfileRaw, loadSession, profileTemplate, saveProfile, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, buildFormTemplate, applyFormFile, promptUnansweredFields, formatStaged, } from "./apply.js";
54
54
  import { createInterface } from "node:readline";
55
55
  import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
56
56
  import { writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
@@ -125,6 +125,8 @@ USAGE
125
125
  write ~/.jobpro/profile.json
126
126
  --interactive fills it via prompts.
127
127
  job-pro profile show print the loaded profile
128
+ job-pro profile lint validate format of every field
129
+ (exits 1 on any FAIL — scriptable)
128
130
  job-pro find <keyword> search ALL 50 companies in parallel
129
131
  [--limit N] [--companies a,b,c]
130
132
  [--timeout ms] [--apply-ready]
@@ -165,6 +167,7 @@ VERBS (same surface for every company)
165
167
  pass "-" to read resume from stdin
166
168
  resume-check <resume-text-or--> structural sanity check on a resume
167
169
  apply <post_id> stage an application (Phase 2 dry-run)
170
+ --schema dump raw schema (no profile needed)
168
171
  --print-form emit a fillable JSON template
169
172
  --form-file <path> merge per-job answers
170
173
  --interactive prompt for unanswered fields
@@ -421,6 +424,7 @@ async function runCompany(adapter, company, rawArgs) {
421
424
  if (verb === "apply") {
422
425
  const reallySubmit = args.includes("--really-submit");
423
426
  const printForm = args.includes("--print-form");
427
+ const schemaOnly = args.includes("--schema");
424
428
  const interactive = args.includes("--interactive");
425
429
  const remember = args.includes("--remember");
426
430
  const { args: aDebug, value: debugUrl } = popFlagValue(args, "--debug-submit-to");
@@ -507,7 +511,7 @@ async function runCompany(adapter, company, rawArgs) {
507
511
  void aBatch;
508
512
  const postId = args[0];
509
513
  if (!postId)
510
- die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path> | --interactive [--remember] | --batch <file>] [--debug-submit-to <url> | --really-submit]`);
514
+ die(`usage: job-pro ${company} apply <post_id> [--schema | --print-form | --form-file <path> | --interactive [--remember] | --batch <file>] [--debug-submit-to <url> | --really-submit]`);
511
515
  const fetchSchema = adapter.fetchApplicationSchema;
512
516
  if (typeof fetchSchema !== "function") {
513
517
  return emit({
@@ -528,6 +532,11 @@ async function runCompany(adapter, company, rawArgs) {
528
532
  if (!sr.ok || !sr.schema) {
529
533
  return emit({ ok: false, source: company, post_id: postId, message: sr.message ?? "unknown error" }, compact);
530
534
  }
535
+ // --schema short-circuits everything (and crucially doesn't need a
536
+ // profile). Useful for recon: "what fields does this job ask?".
537
+ if (schemaOnly) {
538
+ return emit({ ok: true, source: company, post_id: postId, schema: sr.schema }, compact);
539
+ }
531
540
  const prof = loadProfile();
532
541
  if (!prof.ok) {
533
542
  return emit({
@@ -1231,7 +1240,84 @@ async function main() {
1231
1240
  console.log(JSON.stringify(r.profile, null, 2));
1232
1241
  return;
1233
1242
  }
1234
- die(`usage: job-pro profile <init [--interactive] [--force] | show>`);
1243
+ if (sub === "lint") {
1244
+ const compact = args.includes("--compact");
1245
+ const r = loadProfileRaw();
1246
+ if (!r.ok) {
1247
+ console.error(r.message);
1248
+ process.exit(1);
1249
+ }
1250
+ const p = r.profile;
1251
+ const findings = [];
1252
+ // first_name / last_name
1253
+ for (const k of ["first_name", "last_name"]) {
1254
+ const v = (p[k] ?? "").trim();
1255
+ if (!v)
1256
+ findings.push({ level: "FAIL", check: k, message: "missing" });
1257
+ else
1258
+ findings.push({ level: "PASS", check: k, message: v });
1259
+ }
1260
+ // email
1261
+ const email = (p.email ?? "").trim();
1262
+ if (!email)
1263
+ findings.push({ level: "FAIL", check: "email", message: "missing" });
1264
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
1265
+ findings.push({ level: "FAIL", check: "email", message: `"${email}" doesn't look like a valid address` });
1266
+ else
1267
+ findings.push({ level: "PASS", check: "email", message: email });
1268
+ // phone
1269
+ const phone = (p.phone ?? "").trim();
1270
+ if (!phone)
1271
+ findings.push({ level: "FAIL", check: "phone", message: "missing" });
1272
+ else {
1273
+ const digitCount = phone.replace(/\D/g, "").length;
1274
+ if (digitCount < 7)
1275
+ findings.push({ level: "FAIL", check: "phone", message: `"${phone}" has ${digitCount} digit(s); need 7+` });
1276
+ else if (!phone.startsWith("+"))
1277
+ findings.push({ level: "WARN", check: "phone", message: `"${phone}" missing country code (recommended for non-anon adapters; e.g. +86 / +1)` });
1278
+ else
1279
+ findings.push({ level: "PASS", check: "phone", message: phone });
1280
+ }
1281
+ // resume_path
1282
+ const rp = (p.resume_path ?? "").trim();
1283
+ if (!rp)
1284
+ findings.push({ level: "FAIL", check: "resume_path", message: "missing" });
1285
+ else if (!existsSync(rp))
1286
+ findings.push({ level: "FAIL", check: "resume_path", message: `file not found: ${rp}` });
1287
+ else {
1288
+ const lower = rp.toLowerCase();
1289
+ if (!/\.(pdf|docx?|md|txt|rtf)$/i.test(lower))
1290
+ findings.push({ level: "WARN", check: "resume_path", message: `unusual extension: ${rp} (most ATS expect .pdf or .docx)` });
1291
+ else
1292
+ findings.push({ level: "PASS", check: "resume_path", message: rp });
1293
+ }
1294
+ // custom
1295
+ const customCount = Object.keys(p.custom ?? {}).length;
1296
+ if (customCount > 0) {
1297
+ const emptyValues = Object.entries(p.custom ?? {})
1298
+ .filter(([, v]) => typeof v !== "string" || v.trim() === "")
1299
+ .map(([k]) => k);
1300
+ if (emptyValues.length > 0)
1301
+ findings.push({ level: "WARN", check: "custom", message: `${emptyValues.length} empty value(s): ${emptyValues.slice(0, 5).join(", ")}` });
1302
+ else
1303
+ findings.push({ level: "PASS", check: "custom", message: `${customCount} answer(s)` });
1304
+ }
1305
+ const fails = findings.filter((f) => f.level === "FAIL").length;
1306
+ const warns = findings.filter((f) => f.level === "WARN").length;
1307
+ if (compact) {
1308
+ console.log(JSON.stringify({ ok: fails === 0, fails, warns, findings }));
1309
+ }
1310
+ else {
1311
+ const ICON = { PASS: "✓", WARN: "!", FAIL: "✗" };
1312
+ for (const f of findings)
1313
+ console.log(` ${ICON[f.level]} ${f.check.padEnd(13)} ${f.message}`);
1314
+ console.log(`\n ${fails} fail / ${warns} warn / ${findings.length - fails - warns} pass`);
1315
+ }
1316
+ if (fails > 0)
1317
+ process.exit(1);
1318
+ return;
1319
+ }
1320
+ die(`usage: job-pro profile <init [--interactive] [--force] | show | lint>`);
1235
1321
  }
1236
1322
  const adapter = ADAPTERS[cmd];
1237
1323
  if (adapter) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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",