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 +85 -0
- package/dist/index.js +27 -2
- package/package.json +1 -1
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.
|
|
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.
|
|
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",
|