@wport/cli 0.5.0 → 0.6.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,26 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.6.0] — 2026-07-08
9
+
10
+ issue #106 實測回饋:讓 agent 更容易讀懂錯誤與能力邊界,不必踩裸 40x 才知道。
11
+
12
+ ### Added
13
+
14
+ - **`enterprise jobs publish` 失敗明列缺欄**:BR-008 publish gate 回 400(`data.missing_fields`)時,錯誤訊息直接列出缺哪些欄位(如 `Missing required fields: work_hours, headcount`),並提示以 `jobs update` 補齊;不再只有翻譯後的中文一句話。
15
+ - **`enterprise jobs copy` 顯示 `incomplete_fields`**:複製 legacy 職缺(來源子表為空)時,人類模式印出 `⚠ Incomplete for publish — fill before \`jobs publish\`: …`;json 模式回應含 `incomplete_fields`。
16
+ - **`wport doctor` 新增「Enterprise talent scope」區塊**:明示 Enterprise API Key 僅存取「已投遞」人才庫(`talents list --tab visit` 尚未開放、回 400),且**無主動搜尋**——`/api/search-talent` 需互動式 JWT(企業模式)session,API Key → 401(設計如此,非設定錯誤)。
17
+
18
+ ### Changed
19
+
20
+ - `enterprise talents` 群組說明對齊現況:applied pool only、無主動搜尋(詳見 `wport doctor`)。
21
+
22
+ ### Notes
23
+
24
+ - 上述錯誤/邊界提示與後端無耦合,對任何後端版本皆生效;`incomplete_fields` 顯示需後端已含 issue #106 copy 回應擴充(已於 backend `main`)。
25
+
26
+ ---
27
+
8
28
  ## [0.5.0] — 2026-07-07
9
29
 
10
30
  pm_41 企業 CLI 補完:人才庫、召募活動、公司資料、用量查詢,以及職缺複製/關閉。命令樹至此完整覆蓋 PRD §6.1 企業模式全部命令。
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.5.0"} (node ${process.version}; ${process.platform})`;
182
+ return `wport-cli/${"0.6.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) {
@@ -972,13 +972,20 @@ function registerConfigCommand(program2) {
972
972
  // src/commands/doctor.ts
973
973
  var import_node_fs5 = require("fs");
974
974
  var SILENT_IGNORED_PARAMS = ["orderBy", "order"];
975
+ var ENTERPRISE_TALENT_BOUNDARY_NOTES = [
976
+ "`enterprise talents` covers your APPLIED pool only (list / view / respond).",
977
+ "The visit tab is not available yet: `talents list --tab visit` returns 400.",
978
+ "There is NO active candidate search over an API Key. `/api/search-talent` needs an",
979
+ "interactive JWT (company-mode) session and returns 401 for an API Key \u2014 by design,",
980
+ "not a setup error. Do not script talent-sourcing against an Enterprise API Key."
981
+ ];
975
982
  function registerDoctorCommand(program2) {
976
983
  program2.command("doctor").description(
977
984
  "Diagnose CLI setup: resolved config, server reachability, schema fingerprint, and known server quirks."
978
985
  ).action(async (_opts, command) => {
979
986
  const ctx = resolveContext(command);
980
987
  const line = (s = "") => process.stdout.write(s + "\n");
981
- line(`wport-cli ${"0.5.0"}`);
988
+ line(`wport-cli ${"0.6.0"}`);
982
989
  line(` bundled schema fingerprint: ${"839e8a891dfb"}`);
983
990
  line("");
984
991
  line("Resolved configuration:");
@@ -998,6 +1005,9 @@ function registerDoctorCommand(program2) {
998
1005
  line(" \u2022 jobs search sorts by publish date, or by relevance when --keyword is set.");
999
1006
  line(" \u2022 jobs view --batch caps parallelism (default 5, max 20) to stay friendly to the API.");
1000
1007
  line("");
1008
+ line("Enterprise talent scope (Enterprise API Key):");
1009
+ for (const note of ENTERPRISE_TALENT_BOUNDARY_NOTES) line(` \u2022 ${note}`);
1010
+ line("");
1001
1011
  line("Schema drift:");
1002
1012
  line(" The fingerprint above identifies the OpenAPI contract this CLI was built against.");
1003
1013
  line(" Automated drift detection needs a server-side schema-version endpoint, which is");
@@ -1087,8 +1097,25 @@ function throwEnterpriseHttpError(status, body) {
1087
1097
  ExitCode.ServerClientError
1088
1098
  );
1089
1099
  }
1100
+ if (status === 400) {
1101
+ const missingFields = extractMissingFields(body);
1102
+ if (missingFields.length > 0) {
1103
+ throw new CliError(
1104
+ `${base} \u2014 Missing required fields: ${missingFields.join(", ")}. Fill them via \`wport enterprise jobs update <enc_id> ...\` (or the web console), then publish.`,
1105
+ ExitCode.ServerClientError
1106
+ );
1107
+ }
1108
+ }
1090
1109
  throwForHttpStatus(status, body);
1091
1110
  }
1111
+ function extractMissingFields(body) {
1112
+ if (!body || typeof body !== "object") return [];
1113
+ const data = body.data;
1114
+ if (!data || typeof data !== "object") return [];
1115
+ const fields = data.missing_fields;
1116
+ if (!Array.isArray(fields)) return [];
1117
+ return fields.filter((f) => typeof f === "string");
1118
+ }
1092
1119
  function warnIfRateLimitLow(headers) {
1093
1120
  const remaining = Number(headers.get("x-ratelimit-remaining"));
1094
1121
  const limit = Number(headers.get("x-ratelimit-limit"));
@@ -1525,6 +1552,15 @@ async function runJobsCopy(ctx, apiKey, encId, idempotencyKey) {
1525
1552
  }
1526
1553
  process.stdout.write(`Copied job ${trimmed} \u2192 new draft: ${result.enc_id ?? "(unknown)"}
1527
1554
  `);
1555
+ const incomplete = extractStringArray(result.incomplete_fields);
1556
+ if (incomplete.length > 0) {
1557
+ process.stdout.write(` \u26A0 Incomplete for publish \u2014 fill before \`jobs publish\`: ${incomplete.join(", ")}
1558
+ `);
1559
+ }
1560
+ }
1561
+ function extractStringArray(value) {
1562
+ if (!Array.isArray(value)) return [];
1563
+ return value.filter((v) => typeof v === "string");
1528
1564
  }
1529
1565
  function registerEnterpriseJobsCopy(parent) {
1530
1566
  parent.command("copy <enc_id>").description("Copy a job posting into a new unpublished draft").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
@@ -2243,10 +2279,13 @@ async function runEnterpriseTalentsRespond(ctx, apiKey, encResumeId, flags, idem
2243
2279
  throw new CliError("--subject is required and must not be empty", ExitCode.InvalidArgument);
2244
2280
  }
2245
2281
  const body = resolveRespondBody(flags, { timeoutMs: ctx.timeoutMs });
2282
+ const encJobId = flags.encJobId?.trim();
2283
+ const payload = { subject, body };
2284
+ if (encJobId) payload.enc_job_id = encJobId;
2246
2285
  const { body: respBody } = await enterprisePost(
2247
2286
  { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
2248
2287
  `/talents/${encodeURIComponent(trimmedId)}/respond`,
2249
- { subject, body },
2288
+ payload,
2250
2289
  { idempotencyKey }
2251
2290
  );
2252
2291
  const result = unwrapDataResponse(respBody);
@@ -2258,7 +2297,7 @@ async function runEnterpriseTalentsRespond(ctx, apiKey, encResumeId, flags, idem
2258
2297
  `);
2259
2298
  }
2260
2299
  function registerEnterpriseTalentsRespond(parent) {
2261
- parent.command("respond <enc_resume_id>").description("Reply to an applicant (in-app message; inherits the company daily reply quota)").requiredOption("--subject <subject>", "message subject (max 200)").option("--body <text>", "message body text (max 5000)").option("--body-file <path>", 'read message body from a file, or "-" for stdin').option("--idempotency-key <key>", "reuse across retries to avoid duplicate sends (default: a fresh UUID)").action(async (encResumeId, flags, command) => {
2300
+ parent.command("respond <enc_resume_id>").description("Reply to an applicant (in-app message; inherits the company daily reply quota)").requiredOption("--subject <subject>", "message subject (max 200)").option("--body <text>", "message body text (max 5000)").option("--body-file <path>", 'read message body from a file, or "-" for stdin').option("--enc-job-id <enc_id>", "reply to a specific job posting; omit to reply to the most recent application").option("--idempotency-key <key>", "reuse across retries to avoid duplicate sends (default: a fresh UUID)").action(async (encResumeId, flags, command) => {
2262
2301
  const ctx = resolveContext(command);
2263
2302
  const globals = command.optsWithGlobals();
2264
2303
  const { key } = resolveApiKey(globals.apiKey);
@@ -2268,7 +2307,9 @@ function registerEnterpriseTalentsRespond(parent) {
2268
2307
 
2269
2308
  // src/commands/enterprise/talents/index.ts
2270
2309
  function registerEnterpriseTalentsCommand(parent) {
2271
- const talents = parent.command("talents").description("Browse and respond to applicants in your talent pool");
2310
+ const talents = parent.command("talents").description(
2311
+ "Browse & respond to your applied talent pool (visit tab n/a, no active candidate search \u2014 see `wport doctor`)"
2312
+ );
2272
2313
  registerEnterpriseTalentsList(talents);
2273
2314
  registerEnterpriseTalentsView(talents);
2274
2315
  registerEnterpriseTalentsRespond(talents);
@@ -2477,7 +2518,7 @@ function registerEnterpriseCommand(program2) {
2477
2518
 
2478
2519
  // src/index.ts
2479
2520
  var program = new import_commander.Command();
2480
- program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.5.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));
2521
+ program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.6.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));
2481
2522
  registerJobsCommand(program);
2482
2523
  registerConfigCommand(program);
2483
2524
  registerDoctorCommand(program);