job-pro 0.9.8 → 1.0.1

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
@@ -202,6 +202,79 @@ export function formatStaged(s) {
202
202
  function truncate(s, n) {
203
203
  return s.length > n ? s.slice(0, n - 1) + "…" : s;
204
204
  }
205
+ export function buildFormTemplate(schema, profile) {
206
+ const out = [];
207
+ for (const q of schema.questions) {
208
+ for (const f of q.fields) {
209
+ const resolved = resolveAnswer(f, profile);
210
+ out.push({
211
+ name: f.name,
212
+ type: f.type,
213
+ required: q.required,
214
+ label: q.label,
215
+ description: q.description,
216
+ options: f.values && f.values.length > 0 ? f.values : undefined,
217
+ value: resolved.value,
218
+ unanswered_reason: resolved.value ? undefined : resolved.reason || undefined,
219
+ });
220
+ }
221
+ }
222
+ return {
223
+ source: schema.source,
224
+ post_id: schema.post_id,
225
+ job_title: schema.job_title,
226
+ apply_url: schema.apply_url,
227
+ submit_kind: schema.submit_kind,
228
+ fields: out,
229
+ };
230
+ }
231
+ /** Merge a `{ field_name: value }` map into the profile's custom overrides. */
232
+ export function applyFormFile(profile, formFilePath) {
233
+ if (!existsSync(formFilePath)) {
234
+ return { ok: false, message: `form file not found: ${formFilePath}` };
235
+ }
236
+ let raw;
237
+ try {
238
+ raw = readFileSync(formFilePath, "utf8");
239
+ }
240
+ catch (err) {
241
+ return { ok: false, message: `read ${formFilePath} failed: ${err instanceof Error ? err.message : err}` };
242
+ }
243
+ let parsed;
244
+ try {
245
+ parsed = JSON.parse(raw);
246
+ }
247
+ catch (err) {
248
+ return { ok: false, message: `${formFilePath} is not valid JSON: ${err instanceof Error ? err.message : err}` };
249
+ }
250
+ // Accept either:
251
+ // (a) a flat { name: value } map, or
252
+ // (b) the full FormTemplate shape (fields:[{ name, value }, …])
253
+ const overrides = {};
254
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.fields)) {
255
+ for (const f of parsed.fields) {
256
+ if (typeof f.name === "string" && typeof f.value === "string" && f.value.length > 0) {
257
+ overrides[f.name] = f.value;
258
+ }
259
+ }
260
+ }
261
+ else if (parsed && typeof parsed === "object") {
262
+ for (const [k, v] of Object.entries(parsed)) {
263
+ if (typeof v === "string" && v.length > 0)
264
+ overrides[k] = v;
265
+ }
266
+ }
267
+ else {
268
+ return { ok: false, message: "form file must be a JSON object or FormTemplate" };
269
+ }
270
+ return {
271
+ ok: true,
272
+ profile: {
273
+ ...profile,
274
+ custom: { ...(profile.custom ?? {}), ...overrides },
275
+ },
276
+ };
277
+ }
205
278
  export function buildBespokeApplySchema(cfg) {
206
279
  const standard = [
207
280
  { label: "Name", required: true, fields: [{ name: "name", type: "input_text" }] },
package/dist/index.js CHANGED
@@ -50,11 +50,11 @@ 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, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, formatStaged, } from "./apply.js";
53
+ import { loadProfile, loadSession, profileTemplate, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, buildFormTemplate, applyFormFile, formatStaged, } from "./apply.js";
54
54
  import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
55
55
  import { writeFileSync, mkdirSync, existsSync } from "node:fs";
56
56
  import { dirname } from "node:path";
57
- const VERSION = "0.9.8";
57
+ const VERSION = "1.0.1";
58
58
  const COMPANIES = [
59
59
  { key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
60
60
  { key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
@@ -152,8 +152,10 @@ VERBS (same surface for every company)
152
152
  pass "-" to read resume from stdin
153
153
  resume-check <resume-text-or--> structural sanity check on a resume
154
154
  apply <post_id> stage an application (Phase 2 dry-run)
155
- --really-submit is intentionally disabled
156
- until per-ATS flows are validated.
155
+ --print-form emit a fillable JSON template
156
+ --form-file <path> merge per-job answers
157
+ --debug-submit-to <url> verify wire format
158
+ --really-submit actually fire (env-gated)
157
159
  memory list | get <k> | set k=v | event <kind> [payload] | clear
158
160
 
159
161
  OUTPUT
@@ -403,9 +405,12 @@ async function runCompany(adapter, company, rawArgs) {
403
405
  if (verb === "apply") {
404
406
  const postId = args[0];
405
407
  if (!postId)
406
- die(`usage: job-pro ${company} apply <post_id> [--dry-run | --debug-submit-to <url> | --really-submit]`);
408
+ die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path>] [--dry-run | --debug-submit-to <url> | --really-submit]`);
407
409
  const reallySubmit = args.includes("--really-submit");
410
+ const printForm = args.includes("--print-form");
408
411
  const { args: aDebug, value: debugUrl } = popFlagValue(args, "--debug-submit-to");
412
+ const { args: _aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
413
+ void _aForm;
409
414
  const fetchSchema = adapter.fetchApplicationSchema;
410
415
  if (typeof fetchSchema !== "function") {
411
416
  return emit({
@@ -437,7 +442,27 @@ async function runCompany(adapter, company, rawArgs) {
437
442
  hint: `run \`job-pro profile init\` to create a template.`,
438
443
  }, compact);
439
444
  }
440
- const staged = stageApplication(sr.schema, prof.profile);
445
+ // --print-form short-circuits everything else: emit a fillable
446
+ // template specific to this job's schema and exit.
447
+ if (printForm) {
448
+ const template = buildFormTemplate(sr.schema, prof.profile);
449
+ return emit(template, compact);
450
+ }
451
+ // --form-file merges per-job overrides into profile.custom.
452
+ let effectiveProfile = prof.profile;
453
+ if (formFilePath) {
454
+ const merged = applyFormFile(effectiveProfile, formFilePath);
455
+ if (!merged.ok) {
456
+ return emit({
457
+ ok: false,
458
+ source: company,
459
+ post_id: postId,
460
+ message: merged.message,
461
+ }, compact);
462
+ }
463
+ effectiveProfile = merged.profile;
464
+ }
465
+ const staged = stageApplication(sr.schema, effectiveProfile);
441
466
  const session = loadSession(company);
442
467
  // Mode selection: --debug-submit-to <url> overrides everything.
443
468
  if (debugUrl) {
@@ -572,8 +597,16 @@ async function runCompany(adapter, company, rawArgs) {
572
597
  console.log(`\nSession captured (~/.jobpro/${company}.session.json): ${session.cookies.length} cookies + ${Object.keys(session.headers).length} auth headers.`);
573
598
  }
574
599
  if (!staged.ready) {
575
- console.log(`\nFill the unanswered required fields in ${profileTemplate().path} ` +
576
- `(profile.custom.<name> for unknown fields), then re-run.`);
600
+ console.log(`\nFill the unanswered required fields. Easiest path:\n` +
601
+ ` 1. job-pro ${company} apply ${postId} --print-form > form.json\n` +
602
+ ` 2. Edit form.json — set each \`value\` for required fields.\n` +
603
+ ` 3. job-pro ${company} apply ${postId} --form-file form.json\n` +
604
+ `Or paste the following into ${profileTemplate().path} under \`custom\`:`);
605
+ // Emit a copy-pasteable JSON snippet listing each unanswered required.
606
+ const snippet = {};
607
+ for (const f of staged.unanswered_required)
608
+ snippet[f.name] = "";
609
+ console.log(JSON.stringify({ custom: snippet }, null, 2));
577
610
  }
578
611
  else {
579
612
  const isAnon = staged.source.startsWith("boards-api.greenhouse.io/") ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "0.9.8",
3
+ "version": "1.0.1",
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",
@@ -28,6 +28,7 @@
28
28
  "build": "tsc",
29
29
  "dev": "tsx src/index.ts",
30
30
  "test": "tsx test/smoke.ts",
31
+ "test:apply": "tsx test/apply-smoke.ts",
31
32
  "prepublishOnly": "npm run build"
32
33
  },
33
34
  "dependencies": {