@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 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.8.0"} (node ${process.version}; ${process.platform})`;
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.8.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 profile");
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.8.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));
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);