@wport/cli 0.5.0 → 0.7.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,45 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.7.0] — 2026-07-15
9
+
10
+ ### Removed
11
+
12
+ - **`enterprise jobs close` 全面撤銷**(產品決議:職缺狀態只有上下架,無「關閉」態)。
13
+ 「已關閉(status=10)」是 GUI 完全不存在的第三狀態——被 CLI 關閉的職缺會在企業後台
14
+ 顯示成無法再上架、無法操作的殭屍列,卡住整個管理流程。命令改為隱藏 stub:
15
+ 打到時印出遷移訊息(改用 `jobs unpublish` 下架、或 `jobs delete --confirm` 刪除)
16
+ 並 exit 2、不發任何請求。
17
+ - 後端 `PATCH /api/v1/enterprise/jobs/:enc_id/close` 端點同步移除(同一 backend PR)。
18
+ 舊版 CLI(0.5.x–0.6.x)呼叫 `jobs close` 將收到 404——請升級至 0.7.0。
19
+
20
+ ### Notes
21
+
22
+ - 已被關閉的存量職缺由後端資料復原 DML 統一改回「未刊登」(可重新上架),
23
+ 詳見 backend repo `docs/ddl/V20260715*__tsh-pm_41-jobs-close-revoke-restore.sql`。
24
+
25
+ ---
26
+
27
+ ## [0.6.0] — 2026-07-08
28
+
29
+ issue #106 實測回饋:讓 agent 更容易讀懂錯誤與能力邊界,不必踩裸 40x 才知道。
30
+
31
+ ### Added
32
+
33
+ - **`enterprise jobs publish` 失敗明列缺欄**:BR-008 publish gate 回 400(`data.missing_fields`)時,錯誤訊息直接列出缺哪些欄位(如 `Missing required fields: work_hours, headcount`),並提示以 `jobs update` 補齊;不再只有翻譯後的中文一句話。
34
+ - **`enterprise jobs copy` 顯示 `incomplete_fields`**:複製 legacy 職缺(來源子表為空)時,人類模式印出 `⚠ Incomplete for publish — fill before \`jobs publish\`: …`;json 模式回應含 `incomplete_fields`。
35
+ - **`wport doctor` 新增「Enterprise talent scope」區塊**:明示 Enterprise API Key 僅存取「已投遞」人才庫(`talents list --tab visit` 尚未開放、回 400),且**無主動搜尋**——`/api/search-talent` 需互動式 JWT(企業模式)session,API Key → 401(設計如此,非設定錯誤)。
36
+
37
+ ### Changed
38
+
39
+ - `enterprise talents` 群組說明對齊現況:applied pool only、無主動搜尋(詳見 `wport doctor`)。
40
+
41
+ ### Notes
42
+
43
+ - 上述錯誤/邊界提示與後端無耦合,對任何後端版本皆生效;`incomplete_fields` 顯示需後端已含 issue #106 copy 回應擴充(已於 backend `main`)。
44
+
45
+ ---
46
+
8
47
  ## [0.5.0] — 2026-07-07
9
48
 
10
49
  pm_41 企業 CLI 補完:人才庫、召募活動、公司資料、用量查詢,以及職缺複製/關閉。命令樹至此完整覆蓋 PRD §6.1 企業模式全部命令。
package/README.md CHANGED
@@ -69,7 +69,6 @@ wport enterprise jobs view <enc_id>
69
69
  wport enterprise jobs create --file new-job.json
70
70
  wport enterprise jobs publish <enc_id>
71
71
  wport enterprise jobs copy <enc_id>
72
- wport enterprise jobs close <enc_id> --confirm
73
72
 
74
73
  # View / update your company profile
75
74
  wport enterprise company view
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.7.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.7.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) => {
@@ -1533,33 +1569,10 @@ function registerEnterpriseJobsCopy(parent) {
1533
1569
  await runJobsCopy(ctx, key, encId, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
1534
1570
  });
1535
1571
  }
1536
- async function runJobsClose(ctx, apiKey, encId, confirm, idempotencyKey) {
1537
- if (!confirm) {
1538
- throw new CliError(
1539
- "Refusing to close without --confirm (irreversible; a closed job cannot be reopened \u2014 copy it to relist)",
1540
- ExitCode.InvalidArgument
1541
- );
1542
- }
1543
- const trimmed = requireEncId(encId);
1544
- const { body } = await enterprisePatch(
1545
- { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
1546
- `/jobs/${encodeURIComponent(trimmed)}/close`,
1547
- {},
1548
- { idempotencyKey }
1549
- );
1550
- const result = unwrapDataResponse(body);
1551
- if (ctx.format === "json") {
1552
- printJson(result);
1553
- return;
1554
- }
1555
- process.stdout.write(`Closed job: ${result.enc_id ?? trimmed}
1556
- `);
1557
- }
1572
+ var JOBS_CLOSE_REMOVED_MESSAGE = '`jobs close` was removed in @wport/cli 0.7.0 \u2014 the "closed" job state has been revoked product-wide (jobs are only published/unpublished). Use `jobs unpublish <enc_id>` to take a job off the board, or `jobs delete <enc_id> --confirm` to remove it.';
1558
1573
  function registerEnterpriseJobsClose(parent) {
1559
- parent.command("close <enc_id>").description("Close (finalize) a job posting (destructive, irreversible; requires --confirm)").option("--confirm", "confirm this irreversible close (a closed job cannot be reopened)").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
1560
- const ctx = resolveContext(command);
1561
- const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
1562
- await runJobsClose(ctx, key, encId, flags.confirm === true, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
1574
+ parent.command("close [enc_id]", { hidden: true }).description("(removed in 0.7.0)").allowUnknownOption(true).action(async () => {
1575
+ throw new CliError(JOBS_CLOSE_REMOVED_MESSAGE, ExitCode.InvalidArgument);
1563
1576
  });
1564
1577
  }
1565
1578
  function registerEnterpriseJobsPublish(parent) {
@@ -2243,10 +2256,13 @@ async function runEnterpriseTalentsRespond(ctx, apiKey, encResumeId, flags, idem
2243
2256
  throw new CliError("--subject is required and must not be empty", ExitCode.InvalidArgument);
2244
2257
  }
2245
2258
  const body = resolveRespondBody(flags, { timeoutMs: ctx.timeoutMs });
2259
+ const encJobId = flags.encJobId?.trim();
2260
+ const payload = { subject, body };
2261
+ if (encJobId) payload.enc_job_id = encJobId;
2246
2262
  const { body: respBody } = await enterprisePost(
2247
2263
  { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
2248
2264
  `/talents/${encodeURIComponent(trimmedId)}/respond`,
2249
- { subject, body },
2265
+ payload,
2250
2266
  { idempotencyKey }
2251
2267
  );
2252
2268
  const result = unwrapDataResponse(respBody);
@@ -2258,7 +2274,7 @@ async function runEnterpriseTalentsRespond(ctx, apiKey, encResumeId, flags, idem
2258
2274
  `);
2259
2275
  }
2260
2276
  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) => {
2277
+ 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
2278
  const ctx = resolveContext(command);
2263
2279
  const globals = command.optsWithGlobals();
2264
2280
  const { key } = resolveApiKey(globals.apiKey);
@@ -2268,7 +2284,9 @@ function registerEnterpriseTalentsRespond(parent) {
2268
2284
 
2269
2285
  // src/commands/enterprise/talents/index.ts
2270
2286
  function registerEnterpriseTalentsCommand(parent) {
2271
- const talents = parent.command("talents").description("Browse and respond to applicants in your talent pool");
2287
+ const talents = parent.command("talents").description(
2288
+ "Browse & respond to your applied talent pool (visit tab n/a, no active candidate search \u2014 see `wport doctor`)"
2289
+ );
2272
2290
  registerEnterpriseTalentsList(talents);
2273
2291
  registerEnterpriseTalentsView(talents);
2274
2292
  registerEnterpriseTalentsRespond(talents);
@@ -2477,7 +2495,7 @@ function registerEnterpriseCommand(program2) {
2477
2495
 
2478
2496
  // src/index.ts
2479
2497
  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));
2498
+ program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.7.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
2499
  registerJobsCommand(program);
2482
2500
  registerConfigCommand(program);
2483
2501
  registerDoctorCommand(program);