@wport/cli 0.8.0 → 0.9.0
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/CHANGELOG.md +15 -0
- package/README.md +5 -0
- package/dist/index.js +160 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [0.9.0] — 2026-08-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **個人線(pm_47)· `wport personal apply` 投遞**(對接 talent `/api/v1/personal/applications`):
|
|
13
|
+
- 單筆:`personal apply <job_enc_id> --resume <enc_id> --message <text> | --message-file <path|-> --confirm`
|
|
14
|
+
——message 互斥二擇一必填、trim 後 1~2000 字**本地先驗**(超長/空 exit 2 不發請求)、`-` 讀 stdin;
|
|
15
|
+
投遞對雇主可見,無 `--confirm` 一律 exit 2 不發請求;自動附 `Idempotency-Key`。
|
|
16
|
+
- 批次:`personal apply --file <batch.json> --confirm`——`{"applications":[…]}` ≤20 筆與逐筆 message
|
|
17
|
+
本地先驗(超限 exit 2);輸出 succeeded/failed 表,`--output json` 給完整陣列。
|
|
18
|
+
- **錯誤分流**:冷卻 409(印 `next_apply_at`)、職缺終態 409、日上限 429(印剩餘額度 / `resets_at`)
|
|
19
|
+
→ 統一 exit 3 + 人類可讀;其餘 4xx 沿既有 exit code 契約。
|
|
20
|
+
- codegen:凍結 `docs/handoff/swagger-wport-personal-api-v1.{json,yaml}` 納入 applications 端點,
|
|
21
|
+
`PERSONAL_APPLICATIONS_BASE` 常數;`gen:openapi:personal` 經 `--keep-prefix` 自動含 applications。
|
|
22
|
+
|
|
8
23
|
## [0.8.0] — 2026-07-30
|
|
9
24
|
|
|
10
25
|
### Added
|
package/README.md
CHANGED
|
@@ -139,6 +139,11 @@ wport personal resumes publish <enc_id> # searchable by companies (
|
|
|
139
139
|
wport personal resumes delete <enc_id> --confirm
|
|
140
140
|
wport personal resumes export --out ./backup
|
|
141
141
|
|
|
142
|
+
# Apply to jobs (visible to the employer; requires --confirm; 20/day)
|
|
143
|
+
wport personal apply <job_enc_id> --resume <enc_id> --message "自我介紹…" --confirm
|
|
144
|
+
wport personal apply <job_enc_id> --resume <enc_id> --message-file intro.txt --confirm # or - for stdin
|
|
145
|
+
wport personal apply --file batch.json --confirm # { "applications": [...] }, max 20, partial success
|
|
146
|
+
|
|
142
147
|
# Sign out (revokes the refresh token server-side, clears local credentials)
|
|
143
148
|
wport logout
|
|
144
149
|
```
|
package/dist/index.js
CHANGED
|
@@ -179,7 +179,7 @@ function isTimeoutAbort(err) {
|
|
|
179
179
|
return false;
|
|
180
180
|
}
|
|
181
181
|
function buildUserAgent() {
|
|
182
|
-
return `wport-cli/${"0.
|
|
182
|
+
return `wport-cli/${"0.9.0"} (node ${process.version}; ${process.platform})`;
|
|
183
183
|
}
|
|
184
184
|
function unwrapDataResponse(body) {
|
|
185
185
|
if (body && typeof body === "object" && "success" in body && "data" in body) {
|
|
@@ -1148,6 +1148,7 @@ function ensureFormat(key, source) {
|
|
|
1148
1148
|
|
|
1149
1149
|
// src/lib/personal-types.ts
|
|
1150
1150
|
var PERSONAL_RESUMES_BASE = "/api/v1/personal/resumes";
|
|
1151
|
+
var PERSONAL_APPLICATIONS_BASE = "/api/v1/personal/applications";
|
|
1151
1152
|
var OAUTH_BASE = "/api/oauth";
|
|
1152
1153
|
var OAUTH_SESSIONS_BASE = `${OAUTH_BASE}/sessions`;
|
|
1153
1154
|
var SECTION_WRITE_PLAN = {
|
|
@@ -1252,7 +1253,7 @@ function registerDoctorCommand(program2) {
|
|
|
1252
1253
|
}
|
|
1253
1254
|
async function runDoctor(ctx) {
|
|
1254
1255
|
const line = (s = "") => process.stdout.write(s + "\n");
|
|
1255
|
-
line(`wport-cli ${"0.
|
|
1256
|
+
line(`wport-cli ${"0.9.0"}`);
|
|
1256
1257
|
line(` bundled schema fingerprint: ${"839e8a891dfb"}`);
|
|
1257
1258
|
line("");
|
|
1258
1259
|
line("Resolved configuration:");
|
|
@@ -4542,15 +4543,170 @@ function registerPersonalResumesCommand(parent) {
|
|
|
4542
4543
|
registerPersonalResumesDelete(resumes);
|
|
4543
4544
|
}
|
|
4544
4545
|
|
|
4546
|
+
// src/commands/personal/apply/index.ts
|
|
4547
|
+
var import_node_crypto15 = require("crypto");
|
|
4548
|
+
var import_node_fs11 = require("fs");
|
|
4549
|
+
|
|
4550
|
+
// src/commands/personal/apply/message-input.ts
|
|
4551
|
+
var import_node_fs10 = require("fs");
|
|
4552
|
+
var MAX_MESSAGE = 2e3;
|
|
4553
|
+
function defaultReadStdin() {
|
|
4554
|
+
return (0, import_node_fs10.readFileSync)(0, "utf8");
|
|
4555
|
+
}
|
|
4556
|
+
function validateMessageText(raw) {
|
|
4557
|
+
const trimmed = raw.trim();
|
|
4558
|
+
if (trimmed.length < 1) {
|
|
4559
|
+
throw new CliError("application message must not be empty (1~2000 chars) \u61C9\u5FB5\u8A0A\u606F\u5FC5\u586B", ExitCode.InvalidArgument);
|
|
4560
|
+
}
|
|
4561
|
+
if (trimmed.length > MAX_MESSAGE) {
|
|
4562
|
+
throw new CliError(`application message exceeds ${MAX_MESSAGE} chars \u61C9\u5FB5\u8A0A\u606F\u4E0D\u53EF\u8D85\u904E 2000 \u5B57`, ExitCode.InvalidArgument);
|
|
4563
|
+
}
|
|
4564
|
+
return trimmed;
|
|
4565
|
+
}
|
|
4566
|
+
function resolveMessage(opts) {
|
|
4567
|
+
const hasMsg = opts.message !== void 0;
|
|
4568
|
+
const hasFile = opts.messageFile !== void 0;
|
|
4569
|
+
if (hasMsg && hasFile) {
|
|
4570
|
+
throw new CliError("--message and --message-file are mutually exclusive \u4E92\u65A5", ExitCode.InvalidArgument);
|
|
4571
|
+
}
|
|
4572
|
+
if (!hasMsg && !hasFile) {
|
|
4573
|
+
throw new CliError("--message or --message-file is required \u61C9\u5FB5\u8A0A\u606F\u5FC5\u586B", ExitCode.InvalidArgument);
|
|
4574
|
+
}
|
|
4575
|
+
if (hasMsg) return validateMessageText(opts.message);
|
|
4576
|
+
const file = opts.messageFile;
|
|
4577
|
+
const raw = file === "-" ? (opts.readStdin ?? defaultReadStdin)() : (0, import_node_fs10.readFileSync)(file, "utf8");
|
|
4578
|
+
return validateMessageText(raw);
|
|
4579
|
+
}
|
|
4580
|
+
function validateBatchItems(items) {
|
|
4581
|
+
if (!Array.isArray(items) || items.length < 1) {
|
|
4582
|
+
throw new CliError('batch file must contain a non-empty "applications" array', ExitCode.InvalidArgument);
|
|
4583
|
+
}
|
|
4584
|
+
if (items.length > 20) {
|
|
4585
|
+
throw new CliError(`batch exceeds 20 items (got ${items.length}) \u6279\u6B21\u4E0A\u9650 20 \u7B46`, ExitCode.InvalidArgument);
|
|
4586
|
+
}
|
|
4587
|
+
return items.map((it, i) => {
|
|
4588
|
+
const o = it;
|
|
4589
|
+
const jobId = typeof o?.enc_job_id === "string" ? o.enc_job_id.trim() : "";
|
|
4590
|
+
const resumeId = typeof o?.enc_resume_id === "string" ? o.enc_resume_id.trim() : "";
|
|
4591
|
+
if (!jobId || !resumeId) {
|
|
4592
|
+
throw new CliError(`batch item #${i}: enc_job_id and enc_resume_id required`, ExitCode.InvalidArgument);
|
|
4593
|
+
}
|
|
4594
|
+
const message = validateMessageText(typeof o?.application_message === "string" ? o.application_message : "");
|
|
4595
|
+
return { enc_job_id: jobId, enc_resume_id: resumeId, application_message: message };
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
// src/commands/personal/apply/index.ts
|
|
4600
|
+
function rethrowApplyError(err) {
|
|
4601
|
+
if (err instanceof ServerClientHttpError) {
|
|
4602
|
+
const body = err.body ?? null;
|
|
4603
|
+
const code = body?.error_code;
|
|
4604
|
+
if (err.status === 409 && code === "application_cooldown") {
|
|
4605
|
+
throw new CliError(`Application in cooldown. Next apply at: ${body?.next_apply_at ?? "unknown"}`, ExitCode.ServerClientError);
|
|
4606
|
+
}
|
|
4607
|
+
if (err.status === 409 && code === "job_not_available") {
|
|
4608
|
+
throw new CliError("Job is no longer available (closed or removed).", ExitCode.ServerClientError);
|
|
4609
|
+
}
|
|
4610
|
+
if (err.status === 429 && code === "apply_daily_limit_reached") {
|
|
4611
|
+
throw new CliError(
|
|
4612
|
+
`Daily application limit reached (${body?.used}/${body?.limit}). Resets at: ${body?.resets_at ?? "tomorrow"}`,
|
|
4613
|
+
ExitCode.ServerClientError
|
|
4614
|
+
);
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
throw err;
|
|
4618
|
+
}
|
|
4619
|
+
async function runPersonalApply(ctx, args) {
|
|
4620
|
+
if (!args.confirm) {
|
|
4621
|
+
throw new CliError(
|
|
4622
|
+
"Refusing to apply without --confirm (application is visible to the employer)",
|
|
4623
|
+
ExitCode.InvalidArgument
|
|
4624
|
+
);
|
|
4625
|
+
}
|
|
4626
|
+
const opts = { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs };
|
|
4627
|
+
const body = { enc_job_id: args.encJobId, enc_resume_id: args.encResumeId, application_message: args.message };
|
|
4628
|
+
let result;
|
|
4629
|
+
try {
|
|
4630
|
+
result = await personalPost(opts, PERSONAL_APPLICATIONS_BASE, body, { idempotencyKey: (0, import_node_crypto15.randomUUID)() });
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
rethrowApplyError(err);
|
|
4633
|
+
}
|
|
4634
|
+
const data = result.body?.data ?? {};
|
|
4635
|
+
if (ctx.format === "json") {
|
|
4636
|
+
printJson(data);
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4639
|
+
process.stdout.write(`Applied. application: ${sanitizeForTerminal(String(data.enc_application_id ?? ""))}
|
|
4640
|
+
`);
|
|
4641
|
+
const q = data.quota;
|
|
4642
|
+
if (q) process.stdout.write(`Daily quota: ${q.used}/${q.limit} used, ${q.remaining} remaining
|
|
4643
|
+
`);
|
|
4644
|
+
}
|
|
4645
|
+
function registerPersonalApplyCommand(parent) {
|
|
4646
|
+
parent.command("apply [job_enc_id]").description("Apply to a job (single) or batch via --file (requires --confirm)").option("--resume <enc_id>", "resume enc_id to apply with (single mode)").option("--message <text>", "application message (1~2000 chars)").option("--message-file <path>", "read application message from file (use - for stdin)").option("--file <path>", 'batch JSON file { "applications": [...] } (max 20)').option("--confirm", "confirm this outward-facing action").action(async (jobEncId, flags, command) => {
|
|
4647
|
+
const ctx = resolveContext(command);
|
|
4648
|
+
if (flags.file) {
|
|
4649
|
+
await runPersonalApplyBatch(ctx, { file: flags.file, confirm: flags.confirm === true });
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
if (!jobEncId || !flags.resume) {
|
|
4653
|
+
throw new CliError("single apply requires <job_enc_id> and --resume", ExitCode.InvalidArgument);
|
|
4654
|
+
}
|
|
4655
|
+
const message = resolveMessage({ message: flags.message, messageFile: flags.messageFile });
|
|
4656
|
+
await runPersonalApply(ctx, {
|
|
4657
|
+
encJobId: jobEncId,
|
|
4658
|
+
encResumeId: flags.resume,
|
|
4659
|
+
message,
|
|
4660
|
+
confirm: flags.confirm === true
|
|
4661
|
+
});
|
|
4662
|
+
});
|
|
4663
|
+
}
|
|
4664
|
+
async function runPersonalApplyBatch(ctx, args) {
|
|
4665
|
+
if (!args.confirm) {
|
|
4666
|
+
throw new CliError(
|
|
4667
|
+
"Refusing to apply without --confirm (applications are visible to employers)",
|
|
4668
|
+
ExitCode.InvalidArgument
|
|
4669
|
+
);
|
|
4670
|
+
}
|
|
4671
|
+
let parsed;
|
|
4672
|
+
try {
|
|
4673
|
+
parsed = JSON.parse((0, import_node_fs11.readFileSync)(args.file, "utf8"));
|
|
4674
|
+
} catch {
|
|
4675
|
+
throw new CliError(`Cannot read/parse batch file: ${args.file}`, ExitCode.InvalidArgument);
|
|
4676
|
+
}
|
|
4677
|
+
const items = validateBatchItems(parsed?.applications);
|
|
4678
|
+
const opts = { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs };
|
|
4679
|
+
let result;
|
|
4680
|
+
try {
|
|
4681
|
+
result = await personalPost(opts, `${PERSONAL_APPLICATIONS_BASE}/batch`, { applications: items }, { idempotencyKey: (0, import_node_crypto15.randomUUID)() });
|
|
4682
|
+
} catch (err) {
|
|
4683
|
+
rethrowApplyError(err);
|
|
4684
|
+
}
|
|
4685
|
+
const data = result.body?.data ?? {};
|
|
4686
|
+
if (ctx.format === "json") {
|
|
4687
|
+
printJson(data);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
const succeeded = data.succeeded ?? [];
|
|
4691
|
+
const failed = data.failed ?? [];
|
|
4692
|
+
process.stdout.write(`Batch applied: ${succeeded.length} succeeded, ${failed.length} failed
|
|
4693
|
+
`);
|
|
4694
|
+
for (const f of failed) {
|
|
4695
|
+
process.stdout.write(` #${f.index} ${sanitizeForTerminal(f.enc_job_id)}: ${sanitizeForTerminal(f.error_code)}
|
|
4696
|
+
`);
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4545
4700
|
// src/commands/personal/index.ts
|
|
4546
4701
|
function registerPersonalCommand(program2) {
|
|
4547
|
-
const personal = program2.command("personal").description("Manage your personal resumes and
|
|
4702
|
+
const personal = program2.command("personal").description("Manage your personal resumes, profile and applications");
|
|
4548
4703
|
registerPersonalResumesCommand(personal);
|
|
4704
|
+
registerPersonalApplyCommand(personal);
|
|
4549
4705
|
}
|
|
4550
4706
|
|
|
4551
4707
|
// src/index.ts
|
|
4552
4708
|
var program = new import_commander.Command();
|
|
4553
|
-
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.
|
|
4709
|
+
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.9.0", "-v, --version", "output the CLI version").option("--lang <locale>", "Accept-Language locale: zh-TW | en-US | vi-VN | th-TH | id-ID").option("--api <url>", "override API base URL").option("--output <fmt>", "output format: table | json").option("--no-color", "disable color output").option("--timeout <ms>", "HTTP timeout in milliseconds", (v) => Number(v));
|
|
4554
4710
|
registerJobsCommand(program);
|
|
4555
4711
|
registerConfigCommand(program);
|
|
4556
4712
|
registerDoctorCommand(program);
|