@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 +39 -0
- package/README.md +0 -1
- package/dist/index.js +50 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1560
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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);
|