job-pro 1.0.1 → 1.0.2

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
@@ -228,6 +228,91 @@ export function buildFormTemplate(schema, profile) {
228
228
  fields: out,
229
229
  };
230
230
  }
231
+ /**
232
+ * Walk an ApplyFormSchema and prompt for each unanswered required field
233
+ * on stdin (via readline). Returns the new overrides as a flat
234
+ * `{ name: value }` map ready to merge into profile.custom.
235
+ *
236
+ * Behaviour:
237
+ * - Fields already resolved from profile (name/email/phone/resume/etc.)
238
+ * are skipped silently.
239
+ * - For `*_select` field types, options are presented as a numbered
240
+ * list — user can type the index or the literal value.
241
+ * - User can hit Enter to skip a non-required field.
242
+ * - User can type `q` / Ctrl-D to abort; we return what we've got so far.
243
+ *
244
+ * This function intentionally lives in apply.ts (not index.ts) so it
245
+ * stays unit-testable and so a future TUI can swap it out.
246
+ */
247
+ export async function promptUnansweredFields(schema, profile, io) {
248
+ const overrides = {};
249
+ for (const q of schema.questions) {
250
+ // Only prompt for the primary field of each question. Secondary
251
+ // alternates (e.g. `resume_text` alongside `resume`) get the same
252
+ // resolution as the primary and don't need a separate prompt.
253
+ const f = q.fields[0];
254
+ if (!f)
255
+ continue;
256
+ const resolved = resolveAnswer(f, profile);
257
+ if (resolved.value)
258
+ continue; // already filled
259
+ if (!q.required)
260
+ continue; // skip optional fields entirely
261
+ while (true) {
262
+ // Build the prompt.
263
+ const lines = [];
264
+ lines.push(`\n${q.label} (required) [${f.name}]`);
265
+ if (q.description)
266
+ lines.push(` ${q.description}`);
267
+ if (f.values && f.values.length > 0) {
268
+ lines.push(" Options:");
269
+ f.values.forEach((opt, i) => {
270
+ const label = opt.label && opt.label !== opt.value ? `${opt.value} — ${opt.label}` : opt.value;
271
+ lines.push(` [${i + 1}] ${label}`);
272
+ });
273
+ lines.push(" Enter number or value:");
274
+ }
275
+ else if (f.type === "input_file") {
276
+ lines.push(" Enter absolute file path:");
277
+ }
278
+ else if (f.type === "textarea") {
279
+ lines.push(" Enter text (single line; \\n for newlines):");
280
+ }
281
+ else {
282
+ lines.push(" Enter value:");
283
+ }
284
+ lines.push("> ");
285
+ io.write(lines.join("\n"));
286
+ const answer = await io.read();
287
+ if (answer === null) {
288
+ // Ctrl-D / EOF — bail with what we have.
289
+ return overrides;
290
+ }
291
+ const trimmed = answer.trim();
292
+ if (trimmed === "q")
293
+ return overrides;
294
+ if (!trimmed) {
295
+ // Empty input for a required field — re-prompt unless user wants to skip.
296
+ io.write(" (required — type a value, `q` to abort, or `skip` to leave blank)\n");
297
+ continue;
298
+ }
299
+ if (trimmed === "skip")
300
+ break;
301
+ let resolvedAnswer = trimmed;
302
+ if (f.values && f.values.length > 0) {
303
+ const asIdx = Number.parseInt(trimmed, 10);
304
+ if (Number.isFinite(asIdx) && asIdx >= 1 && asIdx <= f.values.length) {
305
+ // Coerce — Greenhouse sometimes ships numeric values that JSON.parse
306
+ // hands back as numbers, breaking .replace below.
307
+ resolvedAnswer = String(f.values[asIdx - 1].value ?? "");
308
+ }
309
+ }
310
+ overrides[f.name] = resolvedAnswer.replace(/\\n/g, "\n");
311
+ break;
312
+ }
313
+ }
314
+ return overrides;
315
+ }
231
316
  /** Merge a `{ field_name: value }` map into the profile's custom overrides. */
232
317
  export function applyFormFile(profile, formFilePath) {
233
318
  if (!existsSync(formFilePath)) {
package/dist/index.js CHANGED
@@ -50,11 +50,12 @@ 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, buildFormTemplate, applyFormFile, formatStaged, } from "./apply.js";
53
+ import { loadProfile, loadSession, profileTemplate, stageApplication, submitApplication, executeFeishu3Step, executeMokaApply, executeBeisenWecruit, executeBeisenITalent, executeCdpRealBrowser, buildFormTemplate, applyFormFile, promptUnansweredFields, formatStaged, } from "./apply.js";
54
+ import { createInterface } from "node:readline";
54
55
  import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
55
56
  import { writeFileSync, mkdirSync, existsSync } from "node:fs";
56
57
  import { dirname } from "node:path";
57
- const VERSION = "1.0.1";
58
+ const VERSION = "1.0.2";
58
59
  const COMPANIES = [
59
60
  { key: "tencent", family: "Bespoke", source: "join.qq.com", label: "Tencent / 腾讯" },
60
61
  { key: "bytedance", family: "Bespoke", source: "jobs.bytedance.com", label: "ByteDance / 字节跳动" },
@@ -154,6 +155,7 @@ VERBS (same surface for every company)
154
155
  apply <post_id> stage an application (Phase 2 dry-run)
155
156
  --print-form emit a fillable JSON template
156
157
  --form-file <path> merge per-job answers
158
+ --interactive prompt for unanswered fields
157
159
  --debug-submit-to <url> verify wire format
158
160
  --really-submit actually fire (env-gated)
159
161
  memory list | get <k> | set k=v | event <kind> [payload] | clear
@@ -408,6 +410,7 @@ async function runCompany(adapter, company, rawArgs) {
408
410
  die(`usage: job-pro ${company} apply <post_id> [--print-form | --form-file <path>] [--dry-run | --debug-submit-to <url> | --really-submit]`);
409
411
  const reallySubmit = args.includes("--really-submit");
410
412
  const printForm = args.includes("--print-form");
413
+ const interactive = args.includes("--interactive");
411
414
  const { args: aDebug, value: debugUrl } = popFlagValue(args, "--debug-submit-to");
412
415
  const { args: _aForm, value: formFilePath } = popFlagValue(aDebug, "--form-file");
413
416
  void _aForm;
@@ -462,6 +465,28 @@ async function runCompany(adapter, company, rawArgs) {
462
465
  }
463
466
  effectiveProfile = merged.profile;
464
467
  }
468
+ // --interactive: prompt stdin for each unanswered required field.
469
+ // Skipped in --compact mode (we'd be polluting JSON output with prompts).
470
+ if (interactive && !compact) {
471
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
472
+ const io = {
473
+ write: (s) => process.stdout.write(s),
474
+ read: () => new Promise((resolve) => {
475
+ rl.once("close", () => resolve(null));
476
+ rl.question("", (a) => resolve(a));
477
+ }),
478
+ };
479
+ console.log(`\nInteractive mode — fill the required fields for "${sr.schema.job_title || postId}".`);
480
+ console.log(`Type \`q\` or Ctrl-D to abort. Hit Enter to skip an optional field.`);
481
+ const overrides = await promptUnansweredFields(sr.schema, effectiveProfile, io);
482
+ rl.close();
483
+ // Merge into effectiveProfile.custom for the rest of the flow.
484
+ effectiveProfile = {
485
+ ...effectiveProfile,
486
+ custom: { ...(effectiveProfile.custom ?? {}), ...overrides },
487
+ };
488
+ console.log(`\nCollected ${Object.keys(overrides).length} answer(s). Staging now…\n`);
489
+ }
465
490
  const staged = stageApplication(sr.schema, effectiveProfile);
466
491
  const session = loadSession(company);
467
492
  // Mode selection: --debug-submit-to <url> overrides everything.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-pro",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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",