job-pro 1.0.15 → 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]
@@ -1238,7 +1240,84 @@ async function main() {
1238
1240
  console.log(JSON.stringify(r.profile, null, 2));
1239
1241
  return;
1240
1242
  }
1241
- 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>`);
1242
1321
  }
1243
1322
  const adapter = ADAPTERS[cmd];
1244
1323
  if (adapter) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "1.0.15",
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",