@wport/cli 0.1.1 → 0.1.3
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 +99 -0
- package/README.md +81 -16
- package/dist/index.js +281 -52
- package/dist/index.js.map +1 -1
- package/package.json +19 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
`@wport/cli` 變更紀錄。格式參考 [Keep a Changelog](https://keepachangelog.com/),
|
|
4
|
+
版號遵循 [Semantic Versioning](https://semver.org/)。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
下一輪變更會寫在這裡。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [0.1.3] — 2026-06-02
|
|
15
|
+
|
|
16
|
+
npm 頁面呈現優化,**無 CLI 邏輯/原始碼變動**(`src/` 行為與 0.1.2 相同)。唯一的執行層差異是版號字串:`tsup.config.ts` 在 build 時把 `pkg.version` 經 `__CLI_VERSION__` define 嵌入 `dist/index.js`,故重 build 後 `wport --version` 會印 `0.1.3`、`dist` 非與 0.1.2 二進位一致(除版號字串外行為相同,比照 0.1.1 的描述)。因 npm 已發布版本不可覆寫、需 republish 才會反映到 npmjs.com,故 bump PATCH。
|
|
17
|
+
|
|
18
|
+
### Docs
|
|
19
|
+
|
|
20
|
+
- **`package.json` metadata 補齊**:新增 `repository`(含 `directory: apps/cli`)、`homepage`、`author`、`bugs`(email channel,與 README `## Support` 一致),讓 npmjs.com sidebar 連結與署名完整。`keywords` 擴充(`ndjson` / `jq` / `agent` / `terminal` / `wport-cli` / `job-search-api`)。
|
|
21
|
+
- **`files` 加入 `CHANGELOG.md`**:changelog 一併進 tarball。
|
|
22
|
+
- **README**:標題下加 npm version / node / license badge;Quick start 後新增 Example output(table + JSON)區塊;開頭 pre-release 免責濃縮;修正 `CHANGELOG.md` 相對連結為絕對 GitHub URL(原連結在 npmjs.com 上 404)。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [0.1.2] — 2026-05-29
|
|
27
|
+
|
|
28
|
+
agent-ready + contract-hardened 一輪。版號維持 PATCH(0.x 階段 SemVer §4 不保證穩定、實際使用者趨近零、移除項有 graceful degradation),但下方 ⚠️ BREAKING 仍明確標注。
|
|
29
|
+
|
|
30
|
+
### ⚠️ BREAKING
|
|
31
|
+
|
|
32
|
+
- **移除 `api_base_url` config key**。改用 `WPORT_API_BASE` 環境變數或 `--api` flag 覆寫 base URL。
|
|
33
|
+
- 理由:把可變 base URL 持久化在磁碟上是 SSRF / credential-exfil 攻擊面,外部使用者並不需要(沒有 localhost / staging 場景);未來加入登入後風險更高。
|
|
34
|
+
- **不會 hard-fail**:既有 config 仍含 `api_base_url` 時,CLI 在載入時印一次 deprecation 警告並忽略該 key(不會 crash)。
|
|
35
|
+
- `wport config set api_base_url <x>` 會明確報錯並引導改用 `WPORT_API_BASE`。
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`jobs search --fields <list>` / `--minimal`**:JSON 輸出時只保留指定欄位(comma-separated dotted paths)。`--minimal` 是 `enc_id,title,company_name,area_display,salary_display` 的捷徑。大幅降低 agent 讀取 response 的 token 量(client-side 過濾,省 token 非省 bytes-on-wire)。`--fields` 與 `--minimal` 互斥;table 模式不受影響並會提示改用 `--output json`。
|
|
40
|
+
- **`jobs view --fields <list>`**:多欄位投影成 JSON object(既有 `--field` 單欄位純值輸出保留)。`--field` 與 `--fields` 互斥。
|
|
41
|
+
- **`jobs view --batch [--concurrency <n>]`**:從 stdin 讀 newline-separated enc_ids(需以 `-` 作 enc_id 參數),bounded 並行(預設 5、最大 20)逐筆查詢,輸出 ND-JSON(每行 `{ enc_id, ok, data? , error? }`)。單筆失敗不影響其餘,輸出順序對齊輸入順序。一個 process 處理多筆,省去 N 次 Node cold start。
|
|
42
|
+
- **`wport doctor`**:診斷指令,列出 resolved config(base URL / locale / timeout / config 檔位置)、server 連通性探測、bundled schema fingerprint,以及「server 會 silent ignore `orderBy` / `order`」等 agent 需知的行為。連不上 server 時 exit `ServerOrNetworkError`(4),其餘 exit 0。
|
|
43
|
+
- **bundled schema fingerprint(`__SCHEMA_HASH__`)**:build 時對 `src/generated/schema.d.ts` 取 SHA-256 前 12 字嵌入,供 `wport doctor` 顯示以人工比對 schema drift(自動偵測待後端提供 schema-version endpoint)。
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **`jobs search --help` 文案**:明講 sort 由 server 控制、`orderBy` / `order` 會被 silently ignored,故 CLI 不開排序 flag。
|
|
48
|
+
- **移除未使用的 `ora` 依賴**(dead dependency;程式碼無任何 import)。
|
|
49
|
+
- **HTTP 連線重用**:明確記錄「交給 Node 內建 fetch(undici)的 global dispatcher connection pooling」,刻意不 ship 自訂 `undici` Agent(near-zero 收益、避免新增 runtime dependency)。
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **piped stdin 讀取的 EAGAIN**:`jobs view -`(與 `--batch`)改用可重試的 `readPipedStdin`,解決上游為另一個 process 時(如 `search | jq | view - --batch`)`readFileSync(0)` 偶發 `EAGAIN: resource temporarily unavailable` 而中斷的問題。
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## [0.1.1] — 2026-05-27
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- **README `## Support` 段聯絡 email**:從 `a39795979@gmail.com`(個人 Gmail,0.1.0 誤用)改成 `yao@wport.me`(公司 email)。`0.1.0` 已 published 到 npm registry、無法事後修改 README 內容,故 bump 0.1.1 republish。
|
|
62
|
+
- PUBLISH-CHECKLIST 與 ROADMAP 對應 §「對外支援 channel」決策紀錄同步修正。
|
|
63
|
+
|
|
64
|
+
### Notes
|
|
65
|
+
|
|
66
|
+
- `0.1.0` 在 npm registry 上仍存在但 README 含 personal Gmail。建議使用者直接 `npm install @wport/cli@0.1.1` 或不指定版本(會解析到 latest = 0.1.1)。維護者後續可能 unpublish `0.1.0`(72h 內、無依賴限制下允許)以完整移除 Gmail 痕跡。
|
|
67
|
+
- 沒有程式碼變更;`dist/index.js` 內容跟 0.1.0 二進位一致(除版號字串)。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## [0.1.0] — TBD(pre-release)
|
|
72
|
+
|
|
73
|
+
對應 PR [#15](https://github.com/w101-admin/W101-TalentSearchHub/pull/15)。
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- `wport jobs search` — 對接公開 `GET /api/jobs/search`:keyword、location、category、page、page-size、`--json-query` 進階逃生口。
|
|
78
|
+
- `wport jobs view <enc_id>` — 對接 `GET /api/jobs/{encId}/view`。支援 `--field <dotted.path>`(例 `--field job_info.salary_display`)、stdin (`-`) 取 enc_id。
|
|
79
|
+
- `wport config <set|get|path|reset>` — 持久化 `locale` / `api_base_url` / `output` / `timeout_ms`。Config 檔權限 0600,位置依 `env-paths` 跨平台。
|
|
80
|
+
- 全域 flags:`--lang`(Accept-Language)、`--api`、`--output`(TTY 偵測 table/json)、`--no-color`、`--timeout`、`-v/--version`、`-h/--help`。
|
|
81
|
+
- OpenAPI codegen pipeline(`gen:openapi:prod` / `gen:openapi:remote`):透過 `scripts/gen-from-remote.cjs` filter `KEEP_PATHS`,跳過 upstream 不相關端點的壞 `$ref`。
|
|
82
|
+
- Typed `paths` schema 從 prod swagger 產生;query 型別取自 `operations['JobsController_searchJobs']['parameters']['query']`,後端 DTO 變動會在 CLI 端 surface 成 TS error。
|
|
83
|
+
- Pagination 對齊 `PaginatedResponse` 扁平結構:`{ success, statusCode, message, data: T[], currentPage, totalPages, pageSize, totalCount }`。
|
|
84
|
+
|
|
85
|
+
### Security
|
|
86
|
+
|
|
87
|
+
- Terminal escape / control-character sanitization,分兩級:
|
|
88
|
+
- `sanitizeForTerminal`(strict,單行):剝全部 C0(含 `\t \n \r`)、DEL、C1、ESC-initiated 序列。printTable cells、`view` 各欄位標題、error/warning 訊息走這條。
|
|
89
|
+
- `sanitizeForTerminalMultiline`(描述本文、`--field` 字串輸出):保留 `\n`,正規化 `\r\n` / lone `\r` 為 `\n`,剝其他控制字元。
|
|
90
|
+
- JSON 模式不需 sanitize:`JSON.stringify` 自動 escape `< 0x20` 字元成 `\uXXXX`。
|
|
91
|
+
- 防禦的攻擊:CSI clear-screen / OSC 0 set-window-title / OSC 8 phishing hyperlink / SGR 偽造 error 訊息 / NUL DEL C1 / CR overwrite / LF row injection / TAB align break。
|
|
92
|
+
|
|
93
|
+
### Notes
|
|
94
|
+
|
|
95
|
+
- 預設 API base URL:`https://api.wport.me`。本機開發用 `wport config set api_base_url http://localhost:3000` 或 `--api` flag 覆寫。
|
|
96
|
+
- 預設 page size:10(對齊後端 `PaginationDto`)。Server 端強制上限 100。
|
|
97
|
+
- 排序由 server 決定:filter 搜尋按 publish date desc、keyword 搜尋按 relevance score。CLI 不暴露 sort flag(後端 service hardcode 不讀 client `orderBy` / `order`)。
|
|
98
|
+
- Rate limit:1200 req/min/IP(read-heavy throttle)。互動使用足夠;不要 scripted multi-process scraping。
|
|
99
|
+
- `--field <path>` 支援 dotted path 因為 `JobViewVM` 是嵌套(`job_info.{job_title,area_display,salary_display,...}` / `company_header_info.company_name` / `job_description`)。
|
package/README.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# @wport/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
> The default API base `https://api.wport.me` and the npm scope `@wport` are
|
|
8
|
-
> **not yet publicly committed contracts**. Both may change before the first
|
|
9
|
-
> public 0.1.0 release. Local development should pin via `wport config set
|
|
10
|
-
> api_base_url <url>` or `--api <url>`.
|
|
11
|
-
>
|
|
12
|
-
> Scope: read-only public job search & view. No login. No employer-side commands yet.
|
|
7
|
+
`wport` — terminal interface to the W101 Talent Search Hub public API. Query public job listings from your shell, pipe results to `jq`, scriptable end-to-end. Read-only public job search & view — no login, no employer-side commands yet.
|
|
8
|
+
|
|
9
|
+
> **Status**: 0.1.x pre-release. The default API base `https://api.wport.me` and the npm scope `@wport` are **not yet publicly committed contracts** and may change before the public 0.1.0 release. Override locally via the `WPORT_API_BASE` env var or `--api <url>`.
|
|
13
10
|
|
|
14
11
|
## Install
|
|
15
12
|
|
|
@@ -31,14 +28,46 @@ wport jobs view <enc_id>
|
|
|
31
28
|
# Pipe to jq for scripting
|
|
32
29
|
wport jobs search --keyword backend --output json | jq '.data[].title'
|
|
33
30
|
|
|
31
|
+
# ...or skip jq: keep only the fields you need (token-friendly for agents)
|
|
32
|
+
wport jobs search --keyword backend --minimal --output json
|
|
33
|
+
wport jobs search --keyword backend --fields enc_id,title --output json
|
|
34
|
+
|
|
34
35
|
# Switch language (affects API response, not CLI strings)
|
|
35
36
|
wport jobs search --keyword backend --lang en-US
|
|
36
37
|
|
|
37
|
-
# Persist your preferences
|
|
38
|
-
# override for local dev)
|
|
38
|
+
# Persist your preferences
|
|
39
39
|
wport config set locale zh-TW
|
|
40
|
-
wport config set api_base_url http://localhost:3000 # only for local dev
|
|
41
40
|
wport config get
|
|
41
|
+
|
|
42
|
+
# Point at a local backend for development (default base is https://api.wport.me)
|
|
43
|
+
WPORT_API_BASE=http://localhost:3000 wport jobs search --keyword backend
|
|
44
|
+
# or per-invocation:
|
|
45
|
+
wport jobs search --keyword backend --api http://localhost:3000
|
|
46
|
+
|
|
47
|
+
# Diagnose your setup (resolved config, connectivity, server quirks)
|
|
48
|
+
wport doctor
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Example output
|
|
52
|
+
|
|
53
|
+
Default `table` output in a TTY (`wport jobs search --keyword backend`):
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
┌────────────────┬──────────────────────────────────────┬──────────────────────┬────────────────────┬────────────────────┬──────────────┐
|
|
57
|
+
│ ENC_ID │ TITLE │ COMPANY │ LOCATION │ SALARY │ UPDATED │
|
|
58
|
+
├────────────────┼──────────────────────────────────────┼──────────────────────┼────────────────────┼────────────────────┼──────────────┤
|
|
59
|
+
│ a1B2c3D4e5F6g… │ Senior Backend Engineer (Node.js) │ Acme Cloud Inc. │ Taipei City │ TWD 1.2M–1.8M / yr │ 2026-05-28 │
|
|
60
|
+
│ h7I8j9K0l1M2n… │ Backend Developer — Go / gRPC │ Penguin Labs │ Remote (TW) │ Negotiable │ 2026-05-27 │
|
|
61
|
+
└────────────────┴──────────────────────────────────────┴──────────────────────┴────────────────────┴────────────────────┴──────────────┘
|
|
62
|
+
Showing page 1/5 (10 of 47 results). Next: wport jobs search --page 2
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Same query as `--output json` (auto-selected when piped), trimmed to two fields for agents:
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
$ wport jobs search --keyword backend --fields enc_id,title --output json | jq '.data[]'
|
|
69
|
+
{ "enc_id": "a1B2c3D4e5F6g7", "title": "Senior Backend Engineer (Node.js)" }
|
|
70
|
+
{ "enc_id": "h7I8j9K0l1M2n3", "title": "Backend Developer — Go / gRPC" }
|
|
42
71
|
```
|
|
43
72
|
|
|
44
73
|
## Commands
|
|
@@ -53,9 +82,13 @@ wport config get
|
|
|
53
82
|
| `-p, --page <n>` | Page number (default 1) |
|
|
54
83
|
| `-s, --page-size <n>` | Page size (default 10, max 100) |
|
|
55
84
|
| `--json-query <file>` | Advanced: read full query body from a JSON file |
|
|
85
|
+
| `--fields <list>` | JSON only: keep only these fields per result (comma-separated dotted paths, e.g. `enc_id,title`) |
|
|
86
|
+
| `--minimal` | JSON only: shorthand for `--fields enc_id,title,company_name,area_display,salary_display` |
|
|
56
87
|
|
|
57
88
|
For query fields not exposed as flags (work_experience_code, remote_mode_codes, salary range, etc.), use `--json-query`. The JSON schema mirrors the server-side `JobSearchDto`.
|
|
58
89
|
|
|
90
|
+
`--fields` / `--minimal` filter **client-side** — they trim the tokens an LLM has to read, not the bytes over the wire (the server has no field-projection param). They apply to JSON output only; in table mode they're ignored (with a notice). `--fields` and `--minimal` are mutually exclusive.
|
|
91
|
+
|
|
59
92
|
**Sort order is server-determined**: results are ordered by publish date (descending) for filter searches, or by relevance score for keyword searches. The endpoint does not honour client-supplied `orderBy` / `order` even though they appear in the schema — this CLI deliberately omits the corresponding flags to avoid misleading you.
|
|
60
93
|
|
|
61
94
|
JSON response shape (paginated, flat):
|
|
@@ -77,10 +110,24 @@ JSON response shape (paginated, flat):
|
|
|
77
110
|
|
|
78
111
|
| Flag | Description |
|
|
79
112
|
|---|---|
|
|
80
|
-
| `--field <path>` | Output a single field (
|
|
113
|
+
| `--field <path>` | Output a single field as a raw value (dotted paths, e.g. `--field job_info.salary_display`) |
|
|
114
|
+
| `--fields <list>` | Output selected fields as a JSON object (comma-separated dotted paths) |
|
|
115
|
+
| `--batch` | Read newline-separated enc_ids from stdin and emit one ND-JSON record per job (requires `-` as the enc_id arg) |
|
|
116
|
+
| `--concurrency <n>` | Max parallel requests in `--batch` mode (default 5, max 20) |
|
|
117
|
+
|
|
118
|
+
`--field` and `--fields` are mutually exclusive.
|
|
81
119
|
|
|
82
120
|
Supports stdin: `echo "<enc_id>" \| wport jobs view -`
|
|
83
121
|
|
|
122
|
+
**Batch mode** processes many jobs in a single process (avoiding N cold starts) and emits ND-JSON — one `{ enc_id, ok, data?, error? }` record per line, in input order. A single failed enc_id becomes an `ok:false` line instead of aborting the run:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# search → extract enc_ids → fetch each job's title in one batched call
|
|
126
|
+
wport jobs search --keyword backend --fields enc_id --output json \
|
|
127
|
+
| jq -r '.data[].enc_id' \
|
|
128
|
+
| wport jobs view - --batch --field job_info.job_title
|
|
129
|
+
```
|
|
130
|
+
|
|
84
131
|
The response is the nested `JobViewVM`:
|
|
85
132
|
|
|
86
133
|
```
|
|
@@ -95,16 +142,34 @@ Table mode prints the highlights from `company_header_info` + `job_info` + the s
|
|
|
95
142
|
|
|
96
143
|
### `wport config <set|get|path|reset>`
|
|
97
144
|
|
|
98
|
-
Configurable keys: `locale`, `
|
|
145
|
+
Configurable keys: `locale`, `output`, `timeout_ms`.
|
|
99
146
|
|
|
100
147
|
Config file location varies by OS (see `wport config path`). Stored with mode `0600`.
|
|
101
148
|
|
|
149
|
+
> **Removed in 0.1.2**: `api_base_url` is no longer a config key (a persisted mutable base URL is an SSRF / credential-exfil surface). Use the `WPORT_API_BASE` env var or `--api` instead. An existing config that still contains `api_base_url` won't break — the key is ignored with a one-time warning.
|
|
150
|
+
|
|
151
|
+
### `wport doctor`
|
|
152
|
+
|
|
153
|
+
Prints a setup diagnostic: resolved configuration (base URL, locale, timeout, config file path), a server connectivity probe, the bundled OpenAPI schema fingerprint, and known server behaviours agents should be aware of (e.g. `orderBy` / `order` are silently ignored). Exits `4` if the server is unreachable, `0` otherwise.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
wport doctor
|
|
157
|
+
WPORT_API_BASE=http://localhost:3000 wport doctor # diagnose a local backend
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Environment variables
|
|
161
|
+
|
|
162
|
+
| Variable | Effect |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `WPORT_API_BASE` | Override the API base URL (http/https only). Precedence: `--api` flag > `WPORT_API_BASE` > default `https://api.wport.me`. |
|
|
165
|
+
| `NO_COLOR` | Disable colored output (standard convention). |
|
|
166
|
+
|
|
102
167
|
## Global flags
|
|
103
168
|
|
|
104
169
|
| Flag | Default |
|
|
105
170
|
|---|---|
|
|
106
171
|
| `--lang <locale>` | config → `zh-TW`. Allowed: `zh-TW`, `en-US`, `vi-VN`, `th-TH`, `id-ID` |
|
|
107
|
-
| `--api <url>` |
|
|
172
|
+
| `--api <url>` | `WPORT_API_BASE` env var → `https://api.wport.me` (production) |
|
|
108
173
|
| `--output <fmt>` | `table` (TTY) / `json` (when piped) |
|
|
109
174
|
| `--no-color` | auto via `NO_COLOR` env |
|
|
110
175
|
| `--timeout <ms>` | 10000 |
|
|
@@ -160,7 +225,7 @@ A public issue tracker (GitHub Issues under [`w101-admin/W101-TalentSearchHub`](
|
|
|
160
225
|
|
|
161
226
|
This CLI tracks the `api.wport.me` public job endpoints (`GET /api/jobs/search`, `GET /api/jobs/{encId}/view`).
|
|
162
227
|
|
|
163
|
-
**Pre-1.0 (0.x.y) — soft compatibility**: During the `0.x.y` phase the upstream API and the CLI may evolve without strict semver guarantees. Backward-incompatible changes can land in any `0.x` minor release. Pin to a specific version (`@wport/cli@0.1.0`) if you script against this CLI in production. Each release pins the server version range it has been tested against (see [`CHANGELOG.md`](CHANGELOG.md)).
|
|
228
|
+
**Pre-1.0 (0.x.y) — soft compatibility**: During the `0.x.y` phase the upstream API and the CLI may evolve without strict semver guarantees. Backward-incompatible changes can land in any `0.x` minor release. Pin to a specific version (`@wport/cli@0.1.0`) if you script against this CLI in production. Each release pins the server version range it has been tested against (see [`CHANGELOG.md`](https://github.com/w101-admin/W101-TalentSearchHub/blob/main/apps/cli/CHANGELOG.md)).
|
|
164
229
|
|
|
165
230
|
**Post-1.0 (1.x.y and beyond) — firm semver**: The `1.0.0` release will commit to standard semver. Any breaking server-side change that affects the CLI surface will be released as a CLI **major** bump (`2.0.0`, `3.0.0`, ...). Additive changes (new flags, new fields) → minor; bug fixes → patch.
|
|
166
231
|
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,9 @@ function isColorEnabled(noColor) {
|
|
|
111
111
|
function printJson(value) {
|
|
112
112
|
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
113
113
|
}
|
|
114
|
+
function printNdjsonLine(value) {
|
|
115
|
+
process.stdout.write(JSON.stringify(value) + "\n");
|
|
116
|
+
}
|
|
114
117
|
function printTable(rows, columns, color) {
|
|
115
118
|
if (rows.length === 0) {
|
|
116
119
|
process.stdout.write(color ? import_picocolors.default.dim("(no results)\n") : "(no results)\n");
|
|
@@ -176,7 +179,7 @@ function isTimeoutAbort(err) {
|
|
|
176
179
|
return false;
|
|
177
180
|
}
|
|
178
181
|
function buildUserAgent() {
|
|
179
|
-
return `wport-cli/${"0.1.
|
|
182
|
+
return `wport-cli/${"0.1.3"} (node ${process.version}; ${process.platform})`;
|
|
180
183
|
}
|
|
181
184
|
function unwrapDataResponse(body) {
|
|
182
185
|
if (body && typeof body === "object" && "success" in body && "data" in body) {
|
|
@@ -232,10 +235,18 @@ var import_node_path = require("path");
|
|
|
232
235
|
var import_env_paths = __toESM(require("env-paths"));
|
|
233
236
|
var ALLOWED_LOCALES = ["zh-TW", "en-US", "vi-VN", "th-TH", "id-ID"];
|
|
234
237
|
var ALLOWED_OUTPUT = ["table", "json"];
|
|
235
|
-
var CONFIG_KEYS = ["locale", "
|
|
238
|
+
var CONFIG_KEYS = ["locale", "output", "timeout_ms"];
|
|
239
|
+
var DEPRECATED_CONFIG_KEYS = ["api_base_url"];
|
|
240
|
+
var DEPRECATED_KEY_HINTS = {
|
|
241
|
+
api_base_url: "Set the WPORT_API_BASE env var (or use --api) instead."
|
|
242
|
+
};
|
|
243
|
+
var deprecationWarned = false;
|
|
236
244
|
function isConfigKey(key) {
|
|
237
245
|
return CONFIG_KEYS.includes(key);
|
|
238
246
|
}
|
|
247
|
+
function isDeprecatedConfigKey(key) {
|
|
248
|
+
return DEPRECATED_CONFIG_KEYS.includes(key);
|
|
249
|
+
}
|
|
239
250
|
var paths = (0, import_env_paths.default)("wport", { suffix: "" });
|
|
240
251
|
function getConfigPath() {
|
|
241
252
|
return (0, import_node_path.join)(paths.config, "config.json");
|
|
@@ -245,6 +256,14 @@ function parseConfig(raw) {
|
|
|
245
256
|
throw new CliError("Config must be a JSON object", ExitCode.ConfigCorrupt);
|
|
246
257
|
}
|
|
247
258
|
const input = raw;
|
|
259
|
+
if (!deprecationWarned) {
|
|
260
|
+
for (const dep of DEPRECATED_CONFIG_KEYS) {
|
|
261
|
+
if (dep in input) {
|
|
262
|
+
deprecationWarned = true;
|
|
263
|
+
printWarn(`Config key "${dep}" was removed in 0.1.2 and is ignored. ${DEPRECATED_KEY_HINTS[dep]}`, false);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
248
267
|
const out = {};
|
|
249
268
|
for (const key of CONFIG_KEYS) {
|
|
250
269
|
if (!(key in input)) continue;
|
|
@@ -317,18 +336,6 @@ function validateAndCoerce(key, value) {
|
|
|
317
336
|
}
|
|
318
337
|
return value;
|
|
319
338
|
}
|
|
320
|
-
case "api_base_url": {
|
|
321
|
-
let url;
|
|
322
|
-
try {
|
|
323
|
-
url = new URL(value);
|
|
324
|
-
} catch {
|
|
325
|
-
throw new CliError(`Invalid URL: ${value}`, ExitCode.InvalidArgument);
|
|
326
|
-
}
|
|
327
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
328
|
-
throw new CliError(`api_base_url must be http or https (got ${url.protocol})`, ExitCode.InvalidArgument);
|
|
329
|
-
}
|
|
330
|
-
return value.replace(/\/$/, "");
|
|
331
|
-
}
|
|
332
339
|
case "output": {
|
|
333
340
|
if (!ALLOWED_OUTPUT.includes(value)) {
|
|
334
341
|
throw new CliError(
|
|
@@ -353,13 +360,14 @@ function validateAndCoerce(key, value) {
|
|
|
353
360
|
|
|
354
361
|
// src/lib/global-opts.ts
|
|
355
362
|
var DEFAULT_BASE_URL = "https://api.wport.me";
|
|
363
|
+
var API_BASE_ENV_VAR = "WPORT_API_BASE";
|
|
356
364
|
var DEFAULT_LOCALE = "zh-TW";
|
|
357
365
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
358
366
|
function resolveContext(command) {
|
|
359
367
|
const globals = command.optsWithGlobals();
|
|
360
368
|
const config = loadConfig();
|
|
361
369
|
return {
|
|
362
|
-
baseUrl: resolveBaseUrl(globals.api
|
|
370
|
+
baseUrl: resolveBaseUrl(globals.api),
|
|
363
371
|
locale: resolveLocale(globals.lang, config),
|
|
364
372
|
timeoutMs: resolveTimeout(globals.timeout, config),
|
|
365
373
|
format: resolveOutputFormat(globals.output),
|
|
@@ -367,12 +375,24 @@ function resolveContext(command) {
|
|
|
367
375
|
config
|
|
368
376
|
};
|
|
369
377
|
}
|
|
370
|
-
function resolveBaseUrl(override
|
|
371
|
-
const
|
|
378
|
+
function resolveBaseUrl(override) {
|
|
379
|
+
const fromEnv = process.env[API_BASE_ENV_VAR]?.trim();
|
|
380
|
+
if (override !== void 0) return validateBaseUrl(override, "--api");
|
|
381
|
+
if (fromEnv) return validateBaseUrl(fromEnv, `${API_BASE_ENV_VAR} env var`);
|
|
382
|
+
return DEFAULT_BASE_URL;
|
|
383
|
+
}
|
|
384
|
+
function validateBaseUrl(raw, source) {
|
|
385
|
+
let url;
|
|
372
386
|
try {
|
|
373
|
-
new URL(raw);
|
|
387
|
+
url = new URL(raw);
|
|
374
388
|
} catch {
|
|
375
|
-
throw new CliError(`Invalid
|
|
389
|
+
throw new CliError(`Invalid API base URL from ${source}: ${raw}`, ExitCode.InvalidArgument);
|
|
390
|
+
}
|
|
391
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
392
|
+
throw new CliError(
|
|
393
|
+
`API base URL from ${source} must be http or https (got ${url.protocol})`,
|
|
394
|
+
ExitCode.InvalidArgument
|
|
395
|
+
);
|
|
376
396
|
}
|
|
377
397
|
return raw.replace(/\/$/, "");
|
|
378
398
|
}
|
|
@@ -391,12 +411,46 @@ function resolveTimeout(override, config) {
|
|
|
391
411
|
return raw;
|
|
392
412
|
}
|
|
393
413
|
|
|
414
|
+
// src/lib/path-utils.ts
|
|
415
|
+
function getPath(obj, dottedPath) {
|
|
416
|
+
const parts = dottedPath.split(".");
|
|
417
|
+
let cur = obj;
|
|
418
|
+
for (const p of parts) {
|
|
419
|
+
if (cur && typeof cur === "object" && Object.prototype.hasOwnProperty.call(cur, p)) {
|
|
420
|
+
cur = cur[p];
|
|
421
|
+
} else {
|
|
422
|
+
return void 0;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return cur;
|
|
426
|
+
}
|
|
427
|
+
function pickPaths(obj, paths2) {
|
|
428
|
+
const out = {};
|
|
429
|
+
for (const p of paths2) {
|
|
430
|
+
const v = getPath(obj, p);
|
|
431
|
+
out[p] = v === void 0 ? null : v;
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
function parseFieldsList(raw) {
|
|
436
|
+
const fields = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
437
|
+
if (fields.length === 0) {
|
|
438
|
+
throw new CliError("--fields requires at least one field name", ExitCode.InvalidArgument);
|
|
439
|
+
}
|
|
440
|
+
return fields;
|
|
441
|
+
}
|
|
442
|
+
|
|
394
443
|
// src/commands/jobs/search.ts
|
|
444
|
+
var MINIMAL_SEARCH_FIELDS = ["enc_id", "title", "company_name", "area_display", "salary_display"];
|
|
395
445
|
function registerJobsSearch(parent) {
|
|
396
446
|
parent.command("search").description(
|
|
397
|
-
"Search public job listings.
|
|
398
|
-
).option("-k, --keyword <text>", "keyword search (title / company name etc.)").option("-l, --location <code...>", "area code (repeatable, e.g. 6001001000)").option("-c, --category <code...>", "job classification code (repeatable)").option("-p, --page <n>", "page number (default 1)", (v) => Number(v)).option("-s, --page-size <n>", "page size (default 10, max 100)", (v) => Number(v)).option("--json-query <file>", "read full query body from JSON file (overrides other flags)").
|
|
447
|
+
"Search public job listings. Sort is server-controlled (publish date, or relevance when --keyword is set); the orderBy / order query params are silently ignored, so this CLI intentionally exposes no sort flags. Run `wport doctor` for the full list of server quirks."
|
|
448
|
+
).option("-k, --keyword <text>", "keyword search (title / company name etc.)").option("-l, --location <code...>", "area code (repeatable, e.g. 6001001000)").option("-c, --category <code...>", "job classification code (repeatable)").option("-p, --page <n>", "page number (default 1)", (v) => Number(v)).option("-s, --page-size <n>", "page size (default 10, max 100)", (v) => Number(v)).option("--json-query <file>", "read full query body from JSON file (overrides other flags)").option(
|
|
449
|
+
"--fields <list>",
|
|
450
|
+
"keep only these fields in each JSON result (comma-separated dotted paths, e.g. enc_id,title). JSON output only."
|
|
451
|
+
).option("--minimal", `shorthand for --fields ${MINIMAL_SEARCH_FIELDS.join(",")}. JSON output only.`).action(async (flags, command) => {
|
|
399
452
|
const ctx = resolveContext(command);
|
|
453
|
+
const fields = resolveSearchFields(flags);
|
|
400
454
|
const query = buildQuery(flags);
|
|
401
455
|
const client = createApiClient({
|
|
402
456
|
baseUrl: ctx.baseUrl,
|
|
@@ -409,9 +463,13 @@ function registerJobsSearch(parent) {
|
|
|
409
463
|
if (!response.ok) throwForHttpStatus(response.status, error);
|
|
410
464
|
const paged = asPaginatedBody(data);
|
|
411
465
|
if (ctx.format === "json") {
|
|
412
|
-
|
|
466
|
+
const body = fields ? { ...paged, data: paged.data.map((item) => pickPaths(item, fields)) } : paged;
|
|
467
|
+
printJson(body);
|
|
413
468
|
return;
|
|
414
469
|
}
|
|
470
|
+
if (fields) {
|
|
471
|
+
printWarn("--fields / --minimal only affect JSON output; ignored for table. Use --output json.", ctx.color);
|
|
472
|
+
}
|
|
415
473
|
printTable(
|
|
416
474
|
paged.data,
|
|
417
475
|
[
|
|
@@ -429,6 +487,14 @@ function registerJobsSearch(parent) {
|
|
|
429
487
|
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
430
488
|
});
|
|
431
489
|
}
|
|
490
|
+
function resolveSearchFields(flags) {
|
|
491
|
+
if (flags.fields && flags.minimal) {
|
|
492
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
493
|
+
}
|
|
494
|
+
if (flags.minimal) return [...MINIMAL_SEARCH_FIELDS];
|
|
495
|
+
if (flags.fields) return parseFieldsList(flags.fields);
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
432
498
|
function buildQuery(flags) {
|
|
433
499
|
if (flags.jsonQuery) {
|
|
434
500
|
return readJsonQuery(flags.jsonQuery);
|
|
@@ -472,35 +538,105 @@ function formatDate(s) {
|
|
|
472
538
|
return m ? m[1] : s;
|
|
473
539
|
}
|
|
474
540
|
|
|
475
|
-
// src/
|
|
476
|
-
|
|
541
|
+
// src/lib/concurrency.ts
|
|
542
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
543
|
+
const results = new Array(items.length);
|
|
544
|
+
let cursor = 0;
|
|
545
|
+
async function worker() {
|
|
546
|
+
for (; ; ) {
|
|
547
|
+
const index = cursor++;
|
|
548
|
+
if (index >= items.length) return;
|
|
549
|
+
results[index] = await fn(items[index], index);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
553
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
554
|
+
return results;
|
|
555
|
+
}
|
|
477
556
|
|
|
478
557
|
// src/lib/io-helpers.ts
|
|
558
|
+
var import_node_fs3 = require("fs");
|
|
479
559
|
function ensureStdinPiped(label) {
|
|
480
560
|
if (process.stdin.isTTY) {
|
|
481
561
|
throw new InvalidArgumentError(`${label}: no data on stdin (run via pipe, or pass the value as an arg)`);
|
|
482
562
|
}
|
|
483
563
|
}
|
|
564
|
+
var DEFAULT_STDIN_TIMEOUT_MS = 3e4;
|
|
565
|
+
function readPipedStdin(label, options = {}) {
|
|
566
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_STDIN_TIMEOUT_MS;
|
|
567
|
+
const now = options.now ?? Date.now;
|
|
568
|
+
ensureStdinPiped(label);
|
|
569
|
+
const chunks = [];
|
|
570
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
571
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
572
|
+
const deadline = now() + timeoutMs;
|
|
573
|
+
for (; ; ) {
|
|
574
|
+
let bytesRead;
|
|
575
|
+
try {
|
|
576
|
+
bytesRead = (0, import_node_fs3.readSync)(0, buf, 0, buf.length, null);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
const code = err.code;
|
|
579
|
+
if (code === "EAGAIN") {
|
|
580
|
+
if (now() > deadline) {
|
|
581
|
+
throw new InvalidArgumentError(`${label}: timed out after ${timeoutMs}ms waiting for piped stdin`);
|
|
582
|
+
}
|
|
583
|
+
Atomics.wait(sleeper, 0, 0, 1);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (code === "EOF") break;
|
|
587
|
+
throw new InvalidArgumentError(`${label}: failed to read stdin: ${err.message}`);
|
|
588
|
+
}
|
|
589
|
+
if (bytesRead === 0) break;
|
|
590
|
+
chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
591
|
+
}
|
|
592
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
593
|
+
}
|
|
484
594
|
|
|
485
595
|
// src/commands/jobs/view.ts
|
|
486
596
|
var import_picocolors2 = __toESM(require("picocolors"));
|
|
597
|
+
var DEFAULT_BATCH_CONCURRENCY = 5;
|
|
598
|
+
var MAX_BATCH_CONCURRENCY = 20;
|
|
487
599
|
function registerJobsView(parent) {
|
|
488
|
-
parent.command("view <enc_id>").description('View a single job. Pass "-" to read enc_id from stdin.').option("--field <path>", "output a single field (
|
|
600
|
+
parent.command("view <enc_id>").description('View a single job. Pass "-" to read enc_id from stdin.').option("--field <path>", "output a single field as a raw value (dotted paths, e.g. job_info.job_title)").option(
|
|
601
|
+
"--fields <list>",
|
|
602
|
+
"output selected fields as a JSON object (comma-separated dotted paths, e.g. job_info.job_title,company_header_info.company_name)"
|
|
603
|
+
).option(
|
|
604
|
+
"--batch",
|
|
605
|
+
'read newline-separated enc_ids from stdin and emit one ND-JSON record per job (requires "-" as the enc_id arg)'
|
|
606
|
+
).option(
|
|
607
|
+
"--concurrency <n>",
|
|
608
|
+
`max parallel requests in --batch mode (default ${DEFAULT_BATCH_CONCURRENCY}, max ${MAX_BATCH_CONCURRENCY})`,
|
|
609
|
+
(v) => Number(v)
|
|
610
|
+
).action(async (encIdArg, flags, command) => {
|
|
489
611
|
const ctx = resolveContext(command);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
612
|
+
if (flags.field && flags.fields) {
|
|
613
|
+
throw new CliError(
|
|
614
|
+
"Use either --field (single raw value) or --fields (JSON object), not both",
|
|
615
|
+
ExitCode.InvalidArgument
|
|
616
|
+
);
|
|
493
617
|
}
|
|
494
618
|
const client = createApiClient({
|
|
495
619
|
baseUrl: ctx.baseUrl,
|
|
496
620
|
locale: ctx.locale,
|
|
497
621
|
timeoutMs: ctx.timeoutMs
|
|
498
622
|
});
|
|
623
|
+
if (flags.batch) {
|
|
624
|
+
await runBatchView(encIdArg, flags, client, ctx.timeoutMs);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const encId = encIdArg === "-" ? readPipedStdin("view -", { timeoutMs: ctx.timeoutMs }).trim() : encIdArg;
|
|
628
|
+
if (!encId) {
|
|
629
|
+
throw new CliError("enc_id is required", ExitCode.InvalidArgument);
|
|
630
|
+
}
|
|
499
631
|
const { data, error, response } = await client.GET("/api/jobs/{encId}/view", {
|
|
500
632
|
params: { path: { encId } }
|
|
501
633
|
});
|
|
502
634
|
if (!response.ok) throwForHttpStatus(response.status, error);
|
|
503
635
|
const job = unwrapDataResponse(data);
|
|
636
|
+
if (flags.fields) {
|
|
637
|
+
printJson(pickPaths(job, parseFieldsList(flags.fields)));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
504
640
|
if (flags.field) {
|
|
505
641
|
const v = getPath(job, flags.field);
|
|
506
642
|
if (v === void 0) {
|
|
@@ -517,6 +653,60 @@ function registerJobsView(parent) {
|
|
|
517
653
|
renderJobTable(job, encId, ctx.color);
|
|
518
654
|
});
|
|
519
655
|
}
|
|
656
|
+
async function runBatchView(encIdArg, flags, client, timeoutMs) {
|
|
657
|
+
if (encIdArg !== "-") {
|
|
658
|
+
throw new CliError('--batch reads enc_ids from stdin; pass "-" as the enc_id argument', ExitCode.InvalidArgument);
|
|
659
|
+
}
|
|
660
|
+
const encIds = parseBatchInput(readPipedStdin("view - --batch", { timeoutMs }));
|
|
661
|
+
if (encIds.length === 0) {
|
|
662
|
+
throw new CliError("No enc_ids found on stdin", ExitCode.InvalidArgument);
|
|
663
|
+
}
|
|
664
|
+
const concurrency = resolveBatchConcurrency(flags.concurrency);
|
|
665
|
+
const project = makeBatchProjector(flags);
|
|
666
|
+
const results = await runBatch(encIds, concurrency, (encId) => fetchJob(client, encId), project);
|
|
667
|
+
for (const record of results) printNdjsonLine(record);
|
|
668
|
+
}
|
|
669
|
+
async function fetchJob(client, encId) {
|
|
670
|
+
const { data, error, response } = await client.GET("/api/jobs/{encId}/view", {
|
|
671
|
+
params: { path: { encId } }
|
|
672
|
+
});
|
|
673
|
+
if (!response.ok) throwForHttpStatus(response.status, error);
|
|
674
|
+
return unwrapDataResponse(data);
|
|
675
|
+
}
|
|
676
|
+
async function runBatch(encIds, concurrency, fetchOne, project) {
|
|
677
|
+
return mapWithConcurrency(encIds, concurrency, async (encId) => {
|
|
678
|
+
try {
|
|
679
|
+
const job = await fetchOne(encId);
|
|
680
|
+
return { enc_id: encId, ok: true, data: project(job) };
|
|
681
|
+
} catch (err) {
|
|
682
|
+
return { enc_id: encId, ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function makeBatchProjector(flags) {
|
|
687
|
+
if (flags.fields) {
|
|
688
|
+
const paths2 = parseFieldsList(flags.fields);
|
|
689
|
+
return (job) => pickPaths(job, paths2);
|
|
690
|
+
}
|
|
691
|
+
if (flags.field) {
|
|
692
|
+
const path = flags.field;
|
|
693
|
+
return (job) => getPath(job, path) ?? null;
|
|
694
|
+
}
|
|
695
|
+
return (job) => job;
|
|
696
|
+
}
|
|
697
|
+
function parseBatchInput(raw) {
|
|
698
|
+
return raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
699
|
+
}
|
|
700
|
+
function resolveBatchConcurrency(raw) {
|
|
701
|
+
const n = raw ?? DEFAULT_BATCH_CONCURRENCY;
|
|
702
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_BATCH_CONCURRENCY) {
|
|
703
|
+
throw new CliError(
|
|
704
|
+
`--concurrency must be an integer between 1 and ${MAX_BATCH_CONCURRENCY} (got ${raw})`,
|
|
705
|
+
ExitCode.InvalidArgument
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return n;
|
|
709
|
+
}
|
|
520
710
|
function renderJobTable(job, encId, color) {
|
|
521
711
|
const label = (s2) => color ? import_picocolors2.default.bold(s2) : s2;
|
|
522
712
|
const s = (v) => v ? sanitizeForTerminal(v) : "";
|
|
@@ -546,29 +736,9 @@ function renderJobTable(job, encId, color) {
|
|
|
546
736
|
function stripHtml(s) {
|
|
547
737
|
return s.replace(/<\/?(p|br|div|li|h[1-6])[^>]*>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\n{3,}/g, "\n\n").trim();
|
|
548
738
|
}
|
|
549
|
-
function getPath(obj, dottedPath) {
|
|
550
|
-
const parts = dottedPath.split(".");
|
|
551
|
-
let cur = obj;
|
|
552
|
-
for (const p of parts) {
|
|
553
|
-
if (cur && typeof cur === "object" && Object.prototype.hasOwnProperty.call(cur, p)) {
|
|
554
|
-
cur = cur[p];
|
|
555
|
-
} else {
|
|
556
|
-
return void 0;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return cur;
|
|
560
|
-
}
|
|
561
739
|
function renderDescription(html) {
|
|
562
740
|
return sanitizeForTerminalMultiline(stripHtml(html));
|
|
563
741
|
}
|
|
564
|
-
function readStdin() {
|
|
565
|
-
ensureStdinPiped("view -");
|
|
566
|
-
try {
|
|
567
|
-
return (0, import_node_fs3.readFileSync)(0, "utf8");
|
|
568
|
-
} catch (err) {
|
|
569
|
-
throw new InvalidArgumentError(`Failed to read enc_id from stdin: ${err.message}`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
742
|
|
|
573
743
|
// src/commands/jobs/index.ts
|
|
574
744
|
function registerJobsCommand(program2) {
|
|
@@ -579,10 +749,16 @@ function registerJobsCommand(program2) {
|
|
|
579
749
|
|
|
580
750
|
// src/commands/config/set.ts
|
|
581
751
|
function registerConfigSet(parent) {
|
|
582
|
-
parent.command("set <key> <value>").description("Set a config value. Keys: locale,
|
|
752
|
+
parent.command("set <key> <value>").description("Set a config value. Keys: locale, output, timeout_ms").action((key, value) => {
|
|
753
|
+
if (isDeprecatedConfigKey(key)) {
|
|
754
|
+
throw new CliError(
|
|
755
|
+
`Config key "${key}" was removed in 0.1.2. Set the ${API_BASE_ENV_VAR} env var (or use the --api flag) instead.`,
|
|
756
|
+
ExitCode.InvalidArgument
|
|
757
|
+
);
|
|
758
|
+
}
|
|
583
759
|
if (!isConfigKey(key)) {
|
|
584
760
|
throw new CliError(
|
|
585
|
-
`Unknown config key "${key}". Allowed: locale,
|
|
761
|
+
`Unknown config key "${key}". Allowed: locale, output, timeout_ms`,
|
|
586
762
|
ExitCode.InvalidArgument
|
|
587
763
|
);
|
|
588
764
|
}
|
|
@@ -605,7 +781,7 @@ function registerConfigGet(parent) {
|
|
|
605
781
|
}
|
|
606
782
|
if (!isConfigKey(key)) {
|
|
607
783
|
throw new CliError(
|
|
608
|
-
`Unknown config key "${key}". Allowed: locale,
|
|
784
|
+
`Unknown config key "${key}". Allowed: locale, output, timeout_ms`,
|
|
609
785
|
ExitCode.InvalidArgument
|
|
610
786
|
);
|
|
611
787
|
}
|
|
@@ -693,11 +869,64 @@ function registerConfigCommand(program2) {
|
|
|
693
869
|
registerConfigReset(config);
|
|
694
870
|
}
|
|
695
871
|
|
|
872
|
+
// src/commands/doctor.ts
|
|
873
|
+
var import_node_fs5 = require("fs");
|
|
874
|
+
var SILENT_IGNORED_PARAMS = ["orderBy", "order"];
|
|
875
|
+
function registerDoctorCommand(program2) {
|
|
876
|
+
program2.command("doctor").description(
|
|
877
|
+
"Diagnose CLI setup: resolved config, server reachability, schema fingerprint, and known server quirks."
|
|
878
|
+
).action(async (_opts, command) => {
|
|
879
|
+
const ctx = resolveContext(command);
|
|
880
|
+
const line = (s = "") => process.stdout.write(s + "\n");
|
|
881
|
+
line(`wport-cli ${"0.1.3"}`);
|
|
882
|
+
line(` bundled schema fingerprint: ${"839e8a891dfb"}`);
|
|
883
|
+
line("");
|
|
884
|
+
line("Resolved configuration:");
|
|
885
|
+
line(` API base URL: ${ctx.baseUrl}`);
|
|
886
|
+
line(` locale: ${ctx.locale}`);
|
|
887
|
+
line(` timeout: ${ctx.timeoutMs}ms`);
|
|
888
|
+
const cfgPath = getConfigPath();
|
|
889
|
+
line(` config file: ${cfgPath}${(0, import_node_fs5.existsSync)(cfgPath) ? "" : " (not present)"}`);
|
|
890
|
+
line("");
|
|
891
|
+
line("Server connectivity:");
|
|
892
|
+
const reachable = await probeServer(ctx, line);
|
|
893
|
+
line("");
|
|
894
|
+
line("Known server behaviours (read this before scripting an agent):");
|
|
895
|
+
line(
|
|
896
|
+
` \u2022 Sort is server-controlled. These query params are silently ignored: ${SILENT_IGNORED_PARAMS.join(", ")}.`
|
|
897
|
+
);
|
|
898
|
+
line(" \u2022 jobs search sorts by publish date, or by relevance when --keyword is set.");
|
|
899
|
+
line(" \u2022 jobs view --batch caps parallelism (default 5, max 20) to stay friendly to the API.");
|
|
900
|
+
line("");
|
|
901
|
+
line("Schema drift:");
|
|
902
|
+
line(" The fingerprint above identifies the OpenAPI contract this CLI was built against.");
|
|
903
|
+
line(" Automated drift detection needs a server-side schema-version endpoint, which is");
|
|
904
|
+
line(" not available yet \u2014 for now, compare fingerprints manually after a server release. [TODO]");
|
|
905
|
+
if (!reachable) process.exit(ExitCode.ServerOrNetworkError);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
async function probeServer(ctx, line) {
|
|
909
|
+
try {
|
|
910
|
+
const client = createApiClient({ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs });
|
|
911
|
+
const { response } = await client.GET("/api/jobs/search", { params: { query: { pageSize: 1 } } });
|
|
912
|
+
if (response.ok) {
|
|
913
|
+
line(` \u2713 reachable (HTTP ${response.status})`);
|
|
914
|
+
} else {
|
|
915
|
+
line(` ! reachable, but server responded HTTP ${response.status}`);
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
} catch (err) {
|
|
919
|
+
line(` \u2717 unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
696
924
|
// src/index.ts
|
|
697
925
|
var program = new import_commander.Command();
|
|
698
|
-
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.1.
|
|
926
|
+
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.1.3", "-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));
|
|
699
927
|
registerJobsCommand(program);
|
|
700
928
|
registerConfigCommand(program);
|
|
929
|
+
registerDoctorCommand(program);
|
|
701
930
|
program.exitOverride();
|
|
702
931
|
program.parseAsync(process.argv).then(() => process.exit(ExitCode.Success)).catch((err) => handleTopLevelError(err));
|
|
703
932
|
function handleTopLevelError(err) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/lib/errors.ts","../src/lib/output.ts","../src/commands/jobs/search.ts","../src/lib/api-client.ts","../src/lib/config-store.ts","../src/lib/global-opts.ts","../src/commands/jobs/view.ts","../src/lib/io-helpers.ts","../src/commands/jobs/index.ts","../src/commands/config/set.ts","../src/commands/config/get.ts","../src/commands/config/path.ts","../src/commands/config/reset.ts","../src/commands/config/index.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { isCliError, ExitCode } from './lib/errors';\nimport { isColorEnabled, printError } from './lib/output';\nimport { registerJobsCommand } from './commands/jobs';\nimport { registerConfigCommand } from './commands/config';\n\nconst program = new Command();\n\nprogram\n\t.name('wport')\n\t.description('wport CLI — terminal interface to the W101 Talent Search Hub public API')\n\t.version(__CLI_VERSION__, '-v, --version', 'output the CLI version')\n\t.option('--lang <locale>', 'Accept-Language locale: zh-TW | en-US | vi-VN | th-TH | id-ID')\n\t.option('--api <url>', 'override API base URL')\n\t.option('--output <fmt>', 'output format: table | json')\n\t.option('--no-color', 'disable color output')\n\t.option('--timeout <ms>', 'HTTP timeout in milliseconds', (v) => Number(v));\n\nregisterJobsCommand(program);\nregisterConfigCommand(program);\n\nprogram.exitOverride();\n\nprogram\n\t.parseAsync(process.argv)\n\t.then(() => process.exit(ExitCode.Success))\n\t.catch((err: unknown) => handleTopLevelError(err));\n\nfunction handleTopLevelError(err: unknown): never {\n\tconst color = isColorEnabled(false);\n\n\t// commander throws CommanderError on its own validation paths (help, version, unknown option).\n\tif (err && typeof err === 'object' && 'code' in err) {\n\t\tconst commanderErr = err as { code?: string; exitCode?: number; message?: string };\n\t\tif (commanderErr.code === 'commander.helpDisplayed' || commanderErr.code === 'commander.version') {\n\t\t\tprocess.exit(ExitCode.Success);\n\t\t}\n\t\t// commander 自己的 exitCode 多半是 1,不在 CLI 的 contract(0/2/3/4/5)內。\n\t\t// 任何 parsing / usage 失敗都當 InvalidArgument(2)。\n\t\tif (commanderErr.message) printError(commanderErr.message, color);\n\t\tprocess.exit(ExitCode.InvalidArgument);\n\t}\n\n\tif (isCliError(err)) {\n\t\tprintError(err.message, color);\n\t\tprocess.exit(err.exitCode);\n\t}\n\n\tconst fallbackMessage = err instanceof Error ? err.message : String(err);\n\tprintError(fallbackMessage, color);\n\tprocess.exit(ExitCode.ServerOrNetworkError);\n}\n","export const ExitCode = {\n\tSuccess: 0,\n\tInvalidArgument: 2,\n\tServerClientError: 3,\n\tServerOrNetworkError: 4,\n\tConfigCorrupt: 5,\n} as const;\n\nexport type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];\n\nexport class CliError extends Error {\n\treadonly exitCode: ExitCodeValue;\n\n\tconstructor(message: string, exitCode: ExitCodeValue) {\n\t\tsuper(message);\n\t\tthis.name = 'CliError';\n\t\tthis.exitCode = exitCode;\n\t}\n}\n\nexport class InvalidArgumentError extends CliError {\n\tconstructor(message: string) {\n\t\tsuper(message, ExitCode.InvalidArgument);\n\t\tthis.name = 'InvalidArgumentError';\n\t}\n}\n\nexport class ServerClientHttpError extends CliError {\n\treadonly status: number;\n\treadonly body: unknown;\n\tconstructor(message: string, status: number, body: unknown) {\n\t\tsuper(message, ExitCode.ServerClientError);\n\t\tthis.name = 'ServerClientHttpError';\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\nexport class NetworkError extends CliError {\n\treadonly cause?: unknown;\n\tconstructor(message: string, cause?: unknown) {\n\t\tsuper(message, ExitCode.ServerOrNetworkError);\n\t\tthis.name = 'NetworkError';\n\t\tthis.cause = cause;\n\t}\n}\n\nexport class ConfigCorruptError extends CliError {\n\treadonly path?: string;\n\tconstructor(message: string, path?: string) {\n\t\tsuper(message, ExitCode.ConfigCorrupt);\n\t\tthis.name = 'ConfigCorruptError';\n\t\tthis.path = path;\n\t}\n}\n\nexport function isCliError(err: unknown): err is CliError {\n\treturn err instanceof CliError;\n}\n","import Table from 'cli-table3';\nimport pc from 'picocolors';\nimport { CliError, ExitCode } from './errors';\n\nexport type OutputFormat = 'table' | 'json';\n\n/**\n * 把 untrusted 字串(從 API 回來的 employer-controlled 內容)變成終端機可安全列印的形式。\n *\n * 防的是 terminal escape injection:\n * - CSI / OSC / DCS 等 ESC 開頭序列(清螢幕、改 title、移動游標、假超連結 phishing 等)\n * - 其他 C0 / C1 控制字元(保留 \\t \\n \\r 三個合法格式化字元)\n *\n * JSON 模式不需要 sanitize:JSON.stringify 會把 < 0x20 的字元 escape 成 \\uXXXX。\n * 只有 table / plain-text 印到 stdout/stderr 的字串走這個 helper。\n *\n * 用 new RegExp(string) 建構,所有控制字元以 \\\\uNNNN 形式撰寫,避免 source 內含 literal 控制字元。\n */\nconst ANSI_ESCAPE_SEQUENCE = new RegExp(\n\t[\n\t\t// CSI: ESC [ params intermediates final\n\t\t'\\\\u001B\\\\[[0-?]*[ -/]*[@-~]',\n\t\t// OSC: ESC ] payload (any chars except BEL/ESC) terminated by BEL or ESC \\\n\t\t'\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n\t\t// Two-char escapes: ESC + Fe final byte (0x40-0x5F = @ A B ... Z [ \\ ] ^ _).\n\t\t// 涵蓋 CSI([) / OSC(]) / DCS(P) / SOS(X) / ST(\\\\) / PM(^) / APC(_) intro。\n\t\t// CSI / OSC regex 在前面 OR-分支會先匹配對應序列;這條兜底所有未覆蓋 Fe。\n\t\t'\\\\u001B[@-_]',\n\t].join('|'),\n\t'g'\n);\n\n// 單行用:剝所有 C0(含 \\t \\n \\r)、DEL、C1。table cell、label-prefixed 標題、\n// error / warning 訊息都走這條 —— 即便 ANSI 已剝乾淨,殘留 \\r 仍能把 cursor 拉回\n// 行首蓋掉前面的內容;\\n 會打斷表格排版、可能偽造後續 row;\\t 寬度可變、\n// 搞壞 cli-table3 對齊。\nconst CONTROL_CHARS_STRICT = new RegExp('[\\\\u0000-\\\\u001F\\\\u007F\\\\u0080-\\\\u009F]', 'g');\n\n// 多行用:內部先把 \\r\\n / lone \\r 正規化成 \\n,再剝其他 C0(含 \\t)、DEL、C1。\n// 保留 \\n 作為合法段落分隔。Caller 不需事先做正規化。\nconst CONTROL_CHARS_MULTILINE = new RegExp('[\\\\u0000-\\\\u0009\\\\u000B-\\\\u001F\\\\u007F\\\\u0080-\\\\u009F]', 'g');\n\nexport function sanitizeForTerminal(s: string): string {\n\treturn s.replace(ANSI_ESCAPE_SEQUENCE, '').replace(CONTROL_CHARS_STRICT, '');\n}\n\nexport function sanitizeForTerminalMultiline(s: string): string {\n\treturn s.replace(ANSI_ESCAPE_SEQUENCE, '').replace(/\\r\\n?/g, '\\n').replace(CONTROL_CHARS_MULTILINE, '');\n}\n\nexport function resolveOutputFormat(explicit: string | undefined): OutputFormat {\n\tif (explicit === undefined) {\n\t\treturn process.stdout.isTTY ? 'table' : 'json';\n\t}\n\tif (explicit !== 'json' && explicit !== 'table') {\n\t\tthrow new CliError(`Invalid --output \"${explicit}\". Allowed: table, json`, ExitCode.InvalidArgument);\n\t}\n\treturn explicit;\n}\n\nexport function isColorEnabled(noColor: boolean | undefined): boolean {\n\tif (noColor === true) return false;\n\tif (process.env.NO_COLOR) return false;\n\treturn process.stdout.isTTY ?? false;\n}\n\nexport function printJson(value: unknown): void {\n\tprocess.stdout.write(JSON.stringify(value, null, 2) + '\\n');\n}\n\nexport interface TableColumn<T> {\n\theader: string;\n\tvalue: (row: T) => string;\n\tmaxWidth?: number;\n}\n\nexport function printTable<T>(rows: T[], columns: TableColumn<T>[], color: boolean): void {\n\tif (rows.length === 0) {\n\t\tprocess.stdout.write(color ? pc.dim('(no results)\\n') : '(no results)\\n');\n\t\treturn;\n\t}\n\tconst table = new Table({\n\t\thead: columns.map((c) => (color ? pc.bold(c.header) : c.header)),\n\t\tstyle: { head: [], border: [] },\n\t\tcolWidths: columns.map((c) => c.maxWidth ?? null),\n\t\twordWrap: true,\n\t});\n\tfor (const row of rows) {\n\t\t// 每個 cell 的字串都過 sanitize:防 employer-controlled 內容(title/company/area/salary 等)注入 escape。\n\t\ttable.push(columns.map((c) => sanitizeForTerminal(c.value(row))));\n\t}\n\tprocess.stdout.write(table.toString() + '\\n');\n}\n\nexport function printError(message: string, color: boolean): void {\n\tconst prefix = color ? pc.red('Error:') : 'Error:';\n\t// Server 回的 error message 也視為 untrusted,sanitize。\n\tprocess.stderr.write(`${prefix} ${sanitizeForTerminal(message)}\\n`);\n}\n\nexport function printWarn(message: string, color: boolean): void {\n\tconst prefix = color ? pc.yellow('Warning:') : 'Warning:';\n\tprocess.stderr.write(`${prefix} ${sanitizeForTerminal(message)}\\n`);\n}\n\nexport function dim(text: string, color: boolean): string {\n\treturn color ? pc.dim(text) : text;\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { asPaginatedBody, createApiClient, throwForHttpStatus } from '../../lib/api-client';\nimport { resolveContext } from '../../lib/global-opts';\nimport { CliError, ExitCode } from '../../lib/errors';\nimport { dim, printJson, printTable } from '../../lib/output';\nimport type { operations } from '../../generated/schema';\n\ninterface SearchFlags {\n\tkeyword?: string;\n\tlocation?: string[];\n\tcategory?: string[];\n\tpage?: number;\n\tpageSize?: number;\n\tjsonQuery?: string;\n}\n\n/**\n * Query type derived from the generated OpenAPI schema for GET /api/jobs/search.\n * Adding a new field on the backend DTO + re-running gen:openapi will make this type\n * widen automatically; any flag we forget to map will surface as a TS error inside\n * buildQuery() instead of being silently dropped at runtime.\n */\ntype SearchQuery = NonNullable<operations['JobsController_searchJobs']['parameters']['query']>;\n\ninterface JobSearchItem {\n\tenc_id?: string;\n\tenc_company_id?: string;\n\tcompany_name?: string;\n\tcompany_logo_url?: string;\n\ttitle?: string;\n\tarea_display?: string;\n\tsalary_display?: string;\n\tsalary_currency_code?: string | null;\n\ttags?: string[];\n\tupdated_at?: string;\n\t[k: string]: unknown;\n}\n\nexport function registerJobsSearch(parent: Command): void {\n\tparent\n\t\t.command('search')\n\t\t.description(\n\t\t\t'Search public job listings. Server sorts by publish date (or relevance when --keyword is set); sort order is not user-configurable.'\n\t\t)\n\t\t.option('-k, --keyword <text>', 'keyword search (title / company name etc.)')\n\t\t.option('-l, --location <code...>', 'area code (repeatable, e.g. 6001001000)')\n\t\t.option('-c, --category <code...>', 'job classification code (repeatable)')\n\t\t.option('-p, --page <n>', 'page number (default 1)', (v) => Number(v))\n\t\t.option('-s, --page-size <n>', 'page size (default 10, max 100)', (v) => Number(v))\n\t\t.option('--json-query <file>', 'read full query body from JSON file (overrides other flags)')\n\t\t.action(async (flags: SearchFlags, command: Command) => {\n\t\t\tconst ctx = resolveContext(command);\n\t\t\tconst query = buildQuery(flags);\n\n\t\t\tconst client = createApiClient({\n\t\t\t\tbaseUrl: ctx.baseUrl,\n\t\t\t\tlocale: ctx.locale,\n\t\t\t\ttimeoutMs: ctx.timeoutMs,\n\t\t\t});\n\n\t\t\t// openapi-fetch consumes the response body itself; use `data` (success) or `error` (non-2xx).\n\t\t\tconst { data, error, response } = await client.GET('/api/jobs/search', {\n\t\t\t\tparams: { query },\n\t\t\t});\n\t\t\tif (!response.ok) throwForHttpStatus(response.status, error);\n\n\t\t\tconst paged = asPaginatedBody<JobSearchItem>(data);\n\n\t\t\tif (ctx.format === 'json') {\n\t\t\t\tprintJson(paged);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tprintTable(\n\t\t\t\tpaged.data,\n\t\t\t\t[\n\t\t\t\t\t{ header: 'ENC_ID', value: (r) => truncate(r.enc_id ?? '', 14) },\n\t\t\t\t\t{ header: 'TITLE', value: (r) => r.title ?? '', maxWidth: 36 },\n\t\t\t\t\t{ header: 'COMPANY', value: (r) => r.company_name ?? '', maxWidth: 20 },\n\t\t\t\t\t{ header: 'LOCATION', value: (r) => r.area_display ?? '', maxWidth: 18 },\n\t\t\t\t\t{ header: 'SALARY', value: (r) => r.salary_display ?? '', maxWidth: 18 },\n\t\t\t\t\t{ header: 'UPDATED', value: (r) => formatDate(r.updated_at), maxWidth: 12 },\n\t\t\t\t],\n\t\t\t\tctx.color\n\t\t\t);\n\n\t\t\tconst head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} results).`;\n\t\t\tconst hint =\n\t\t\t\tpaged.totalPages > paged.currentPage ? ` Next: wport jobs search --page ${paged.currentPage + 1}` : '';\n\t\t\tprocess.stdout.write(dim(head + hint, ctx.color) + '\\n');\n\t\t});\n}\n\nfunction buildQuery(flags: SearchFlags): SearchQuery {\n\tif (flags.jsonQuery) {\n\t\t// User-supplied JSON escape hatch — we trust the caller. Runtime validation will come\n\t\t// from the server side; this is the one spot the typed query is deliberately relaxed.\n\t\treturn readJsonQuery(flags.jsonQuery) as SearchQuery;\n\t}\n\tconst q: SearchQuery = {};\n\tif (flags.keyword) q.keyword = flags.keyword;\n\tif (flags.location?.length) q.area_codes = flags.location;\n\tif (flags.category?.length) q.job_classification_codes = flags.category;\n\tif (flags.page !== undefined) q.currentPage = flags.page;\n\tif (flags.pageSize !== undefined) q.pageSize = flags.pageSize;\n\treturn q;\n}\n\nfunction readJsonQuery(path: string): Record<string, unknown> {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, 'utf8');\n\t} catch (err) {\n\t\tthrow new CliError(`Cannot read --json-query file ${path}: ${(err as Error).message}`, ExitCode.InvalidArgument);\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw);\n\t\tif (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n\t\t\tthrow new Error(\n\t\t\t\tArray.isArray(parsed)\n\t\t\t\t\t? 'JSON root is an array; expected an object with query fields'\n\t\t\t\t\t: 'JSON root must be an object'\n\t\t\t);\n\t\t}\n\t\treturn parsed as Record<string, unknown>;\n\t} catch (err) {\n\t\tthrow new CliError(`Invalid JSON in ${path}: ${(err as Error).message}`, ExitCode.InvalidArgument);\n\t}\n}\n\nfunction truncate(s: string, max: number): string {\n\t// Fast path: UTF-16 code units (string.length) are always >= codepoint count,\n\t// so if byte-length already fits we don't need codepoint counting.\n\tif (s.length <= max) return s;\n\t// Array.from iterates by code-point, so surrogate pairs (emoji, supplementary\n\t// plane chars) aren't split mid-character. Doesn't handle grapheme clusters\n\t// (combining marks), but covers the common bilingual / emoji case.\n\tconst chars = Array.from(s);\n\tif (chars.length <= max) return s;\n\treturn chars.slice(0, max - 1).join('') + '…';\n}\n\nfunction formatDate(s: string | undefined): string {\n\tif (!s) return '';\n\tconst m = /^(\\d{4}-\\d{2}-\\d{2})/.exec(s);\n\treturn m ? m[1] : s;\n}\n","import createClient, { type Client } from 'openapi-fetch';\nimport type { paths } from '../generated/schema';\nimport { CliError, ExitCode, NetworkError, ServerClientHttpError } from './errors';\n\nexport interface ApiClientOptions {\n\tbaseUrl: string;\n\tlocale: string;\n\ttimeoutMs: number;\n}\n\nexport type ApiClient = Client<paths>;\n\nexport function createApiClient(opts: ApiClientOptions): ApiClient {\n\treturn createClient<paths>({\n\t\tbaseUrl: opts.baseUrl,\n\t\theaders: {\n\t\t\t'Accept-Language': opts.locale,\n\t\t\t'User-Agent': buildUserAgent(),\n\t\t\tAccept: 'application/json',\n\t\t},\n\t\tfetch: (request: Request) => fetchWithTimeout(request, opts.timeoutMs),\n\t});\n}\n\nfunction fetchWithTimeout(request: Request, timeoutMs: number): Promise<Response> {\n\t// AbortSignal.timeout is available since Node 17.3 (engines requires >=18.17).\n\t// New Request inherits method/headers/body but overrides signal.\n\tconst timedRequest = new Request(request, { signal: AbortSignal.timeout(timeoutMs) });\n\treturn fetch(timedRequest).catch((err: unknown) => {\n\t\tif (isTimeoutAbort(err)) {\n\t\t\tthrow new NetworkError(`Request timed out after ${timeoutMs}ms`, err);\n\t\t}\n\t\t// TypeError: fetch failed → ECONNREFUSED / ENOTFOUND / TLS / proxy\n\t\tconst code = (err as { cause?: { code?: string } })?.cause?.code;\n\t\tconst detail = code ? code : ((err as Error)?.message ?? String(err));\n\t\tthrow new NetworkError(`Cannot reach upstream: ${detail}`, err);\n\t});\n}\n\nfunction isTimeoutAbort(err: unknown): boolean {\n\tif (err && typeof err === 'object' && 'name' in err) {\n\t\tconst name = (err as { name?: string }).name;\n\t\treturn name === 'TimeoutError' || name === 'AbortError';\n\t}\n\treturn false;\n}\n\nexport function buildUserAgent(): string {\n\treturn `wport-cli/${__CLI_VERSION__} (node ${process.version}; ${process.platform})`;\n}\n\n/**\n * W101 後端 `DataResponse<T>` wrapper:`{ success, statusCode, message, data: T }`。\n * 用於回傳「單筆」payload 的端點(例如 GET /api/jobs/:encId/view)。\n *\n * Shape 不符直接 throw CliError,不再 silent cast 把 wrapper 當 payload 回。\n *\n * 注意:`PaginatedResponse` 是另一種扁平結構\n * `{ success, statusCode, message, data: T[], currentPage, totalPages, pageSize, totalCount }`,\n * 不要對 paginated 回應用此函式 —— 會丟失 pagination 欄位。\n */\nexport function unwrapDataResponse<T>(body: unknown): T {\n\tif (body && typeof body === 'object' && 'success' in body && 'data' in body) {\n\t\treturn (body as { data: T }).data;\n\t}\n\tthrow new CliError('Unexpected response shape: missing { success, data } wrapper', ExitCode.ServerOrNetworkError);\n}\n\n/**\n * W101 後端 `PaginatedResponse<T>` wrapper(扁平):\n * `{ success, statusCode, message, data: T[], currentPage, totalPages, pageSize, totalCount }`。\n * 直接 cast,不剝外層;callers 從 `.data` 拿 list,從頂層欄位拿分頁 metadata。\n */\nexport interface PaginatedBody<T> {\n\tsuccess?: boolean;\n\tstatusCode?: number;\n\tmessage?: unknown;\n\tdata: T[];\n\tcurrentPage: number;\n\ttotalPages: number;\n\tpageSize: number;\n\ttotalCount: number;\n}\n\nexport function asPaginatedBody<T>(body: unknown): PaginatedBody<T> {\n\tif (!body || typeof body !== 'object') {\n\t\tthrow new CliError('Unexpected response shape: not an object', ExitCode.ServerOrNetworkError);\n\t}\n\tconst b = body as Record<string, unknown>;\n\tif (!Array.isArray(b.data)) {\n\t\tthrow new CliError('Unexpected response shape: missing `data` array', ExitCode.ServerOrNetworkError);\n\t}\n\tfor (const key of ['currentPage', 'totalPages', 'pageSize', 'totalCount'] as const) {\n\t\tif (typeof b[key] !== 'number') {\n\t\t\tthrow new CliError(`Unexpected response shape: missing or non-numeric \"${key}\"`, ExitCode.ServerOrNetworkError);\n\t\t}\n\t}\n\treturn body as PaginatedBody<T>;\n}\n\nexport function throwForHttpStatus(status: number, body: unknown): never {\n\tconst message = extractErrorMessage(body) ?? `HTTP ${status}`;\n\tif (status >= 400 && status < 500) {\n\t\tthrow new ServerClientHttpError(message, status, body);\n\t}\n\tthrow new CliError(message, ExitCode.ServerOrNetworkError);\n}\n\nfunction extractErrorMessage(body: unknown): string | null {\n\tif (typeof body === 'string') return body;\n\tif (body && typeof body === 'object') {\n\t\tconst obj = body as Record<string, unknown>;\n\t\t// NestJS class-validator default: message: string[]\n\t\tif (Array.isArray(obj.message)) {\n\t\t\tconst parts = obj.message.filter((m): m is string => typeof m === 'string');\n\t\t\tif (parts.length > 0) return parts.join('; ');\n\t\t}\n\t\tif (typeof obj.message === 'string') return obj.message;\n\t\t// i18n object 或 nested → 印 JSON tail\n\t\tif (obj.message && typeof obj.message === 'object') {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(obj.message);\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t}\n\t\tif (typeof obj.error === 'string') return obj.error;\n\t}\n\treturn null;\n}\n","import {\n\texistsSync,\n\tmkdirSync,\n\treadFileSync,\n\tchmodSync,\n\topenSync,\n\twriteSync,\n\tcloseSync,\n\trenameSync,\n\tunlinkSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport envPaths from 'env-paths';\nimport { CliError, ExitCode } from './errors';\nimport { printWarn } from './output';\n\nexport const ALLOWED_LOCALES = ['zh-TW', 'en-US', 'vi-VN', 'th-TH', 'id-ID'] as const;\nexport type Locale = (typeof ALLOWED_LOCALES)[number];\n\nexport const ALLOWED_OUTPUT = ['table', 'json'] as const;\nexport type OutputPref = (typeof ALLOWED_OUTPUT)[number];\n\nexport interface CliConfig {\n\tlocale?: Locale;\n\tapi_base_url?: string;\n\toutput?: OutputPref;\n\ttimeout_ms?: number;\n}\n\nconst CONFIG_KEYS = ['locale', 'api_base_url', 'output', 'timeout_ms'] as const satisfies readonly (keyof CliConfig)[];\nexport type ConfigKey = (typeof CONFIG_KEYS)[number];\n\n/** Per-key value type. validateAndCoerce<K> returns ConfigValueMap[K]. */\ntype ConfigValueMap = {\n\tlocale: Locale;\n\tapi_base_url: string;\n\toutput: OutputPref;\n\ttimeout_ms: number;\n};\n\nexport function isConfigKey(key: string): key is ConfigKey {\n\treturn (CONFIG_KEYS as readonly string[]).includes(key);\n}\n\nconst paths = envPaths('wport', { suffix: '' });\n\nexport function getConfigPath(): string {\n\treturn join(paths.config, 'config.json');\n}\n\n/**\n * Trust boundary:把外部 JSON object 收成型別正確的 CliConfig。\n * 每個 key 都走 validateAndCoerce,不認識的 key 直接 drop(forward compatible,\n * 未來新版多塞了 key、舊 CLI 不會炸)。\n */\nexport function parseConfig(raw: unknown): CliConfig {\n\tif (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n\t\tthrow new CliError('Config must be a JSON object', ExitCode.ConfigCorrupt);\n\t}\n\tconst input = raw as Record<string, unknown>;\n\tconst out: CliConfig = {};\n\tfor (const key of CONFIG_KEYS) {\n\t\tif (!(key in input)) continue;\n\t\tconst value = input[key];\n\t\ttry {\n\t\t\tconst coerced = validateAndCoerce(key, String(value));\n\t\t\t// Object.assign 形式避免 TS 5.5 在 `out[key] = coerced` 上推不出 ConfigValueMap[K]→CliConfig[K]\n\t\t\t// 的對應關係。validateAndCoerce 已是 generic、型別正確;此處只是 indexed assignment 的繞道。\n\t\t\tObject.assign(out, { [key]: coerced });\n\t\t} catch (err) {\n\t\t\tif (err instanceof CliError) {\n\t\t\t\tthrow new CliError(`Config key \"${key}\" invalid: ${err.message}`, ExitCode.ConfigCorrupt);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn out;\n}\n\nexport function loadConfig(): CliConfig {\n\tconst path = getConfigPath();\n\tif (!existsSync(path)) return {};\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, 'utf8');\n\t} catch (err) {\n\t\tthrow new CliError(`Failed to read config at ${path}: ${(err as Error).message}`, ExitCode.ConfigCorrupt);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (err) {\n\t\tthrow new CliError(`Failed to parse JSON at ${path}: ${(err as Error).message}`, ExitCode.ConfigCorrupt);\n\t}\n\treturn parseConfig(parsed);\n}\n\nexport function saveConfig(config: CliConfig): void {\n\tconst path = getConfigPath();\n\tmkdirSync(dirname(path), { recursive: true });\n\n\t// Atomic: write tmpfile (mode 0o600 from open()) → rename to final path (POSIX atomic).\n\t// 即使中途 crash,舊檔仍完整、不會留 truncated JSON 觸發 ConfigCorrupt 鎖死 CLI。\n\tconst tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;\n\tconst fd = openSync(tmpPath, 'w', 0o600);\n\ttry {\n\t\twriteSync(fd, JSON.stringify(config, null, 2) + '\\n');\n\t} catch (err) {\n\t\tcloseSync(fd);\n\t\ttry {\n\t\t\tunlinkSync(tmpPath);\n\t\t} catch {\n\t\t\t/* best effort cleanup */\n\t\t}\n\t\tthrow err;\n\t}\n\tcloseSync(fd);\n\n\t// POSIX: openSync's mode is masked by umask (umask can only narrow, never widen).\n\t// chmod restores 0o600 in case a permissive umask stripped owner bits; it cannot\n\t// expose the file to group/other. On Windows chmodSync is a no-op, skip entirely.\n\tif (process.platform !== 'win32') {\n\t\ttry {\n\t\t\tchmodSync(tmpPath, 0o600);\n\t\t} catch (err) {\n\t\t\tprintWarn(\n\t\t\t\t`Failed to chmod 0600 on config tmpfile: ${(err as Error).message}. ` +\n\t\t\t\t\t'Other users on this system may be able to read CLI config.',\n\t\t\t\tfalse\n\t\t\t);\n\t\t}\n\t}\n\n\trenameSync(tmpPath, path);\n}\n\nexport function validateAndCoerce<K extends ConfigKey>(key: K, value: string): ConfigValueMap[K] {\n\tswitch (key) {\n\t\tcase 'locale': {\n\t\t\tif (!(ALLOWED_LOCALES as readonly string[]).includes(value)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Invalid locale \"${value}\". Allowed: ${ALLOWED_LOCALES.join(', ')}`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn value as ConfigValueMap[K];\n\t\t}\n\t\tcase 'api_base_url': {\n\t\t\tlet url: URL;\n\t\t\ttry {\n\t\t\t\turl = new URL(value);\n\t\t\t} catch {\n\t\t\t\tthrow new CliError(`Invalid URL: ${value}`, ExitCode.InvalidArgument);\n\t\t\t}\n\t\t\tif (url.protocol !== 'https:' && url.protocol !== 'http:') {\n\t\t\t\tthrow new CliError(`api_base_url must be http or https (got ${url.protocol})`, ExitCode.InvalidArgument);\n\t\t\t}\n\t\t\treturn value.replace(/\\/$/, '') as ConfigValueMap[K];\n\t\t}\n\t\tcase 'output': {\n\t\t\tif (!(ALLOWED_OUTPUT as readonly string[]).includes(value)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Invalid output \"${value}\". Allowed: ${ALLOWED_OUTPUT.join(', ')}`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn value as ConfigValueMap[K];\n\t\t}\n\t\tcase 'timeout_ms': {\n\t\t\tconst n = Number(value);\n\t\t\tif (!Number.isInteger(n) || n < 100 || n > 600_000) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`timeout_ms must be an integer between 100 and 600000 (got ${value})`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn n as ConfigValueMap[K];\n\t\t}\n\t}\n}\n","import type { Command } from 'commander';\nimport { ALLOWED_LOCALES, loadConfig, type Locale, type CliConfig } from './config-store';\nimport { CliError, ExitCode } from './errors';\nimport { resolveOutputFormat, isColorEnabled, type OutputFormat } from './output';\n\n// Production public API. Local development overrides via `wport config set api_base_url`\n// or the `--api` flag.\nconst DEFAULT_BASE_URL = 'https://api.wport.me';\nconst DEFAULT_LOCALE: Locale = 'zh-TW';\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\nexport interface ResolvedContext {\n\tbaseUrl: string;\n\tlocale: Locale;\n\ttimeoutMs: number;\n\tformat: OutputFormat;\n\tcolor: boolean;\n\tconfig: CliConfig;\n}\n\ninterface RawGlobals {\n\tlang?: string;\n\tapi?: string;\n\toutput?: string;\n\tcolor?: boolean;\n\ttimeout?: number;\n}\n\nexport function resolveContext(command: Command): ResolvedContext {\n\tconst globals = command.optsWithGlobals() as RawGlobals;\n\tconst config = loadConfig();\n\n\treturn {\n\t\tbaseUrl: resolveBaseUrl(globals.api, config),\n\t\tlocale: resolveLocale(globals.lang, config),\n\t\ttimeoutMs: resolveTimeout(globals.timeout, config),\n\t\tformat: resolveOutputFormat(globals.output),\n\t\tcolor: isColorEnabled(globals.color === false),\n\t\tconfig,\n\t};\n}\n\nfunction resolveBaseUrl(override: string | undefined, config: CliConfig): string {\n\tconst raw = override ?? config.api_base_url ?? DEFAULT_BASE_URL;\n\ttry {\n\t\tnew URL(raw);\n\t} catch {\n\t\tthrow new CliError(`Invalid api base URL: ${raw}`, ExitCode.InvalidArgument);\n\t}\n\treturn raw.replace(/\\/$/, '');\n}\n\nfunction resolveLocale(override: string | undefined, config: CliConfig): Locale {\n\tconst raw = override ?? config.locale ?? DEFAULT_LOCALE;\n\tif (!(ALLOWED_LOCALES as readonly string[]).includes(raw)) {\n\t\tthrow new CliError(`Invalid --lang \"${raw}\". Allowed: ${ALLOWED_LOCALES.join(', ')}`, ExitCode.InvalidArgument);\n\t}\n\treturn raw as Locale;\n}\n\nfunction resolveTimeout(override: number | undefined, config: CliConfig): number {\n\tconst raw = override ?? config.timeout_ms ?? DEFAULT_TIMEOUT_MS;\n\tif (!Number.isInteger(raw) || raw < 100 || raw > 600_000) {\n\t\tthrow new CliError(`Invalid --timeout ${raw} (must be integer 100..600000)`, ExitCode.InvalidArgument);\n\t}\n\treturn raw;\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { createApiClient, throwForHttpStatus, unwrapDataResponse } from '../../lib/api-client';\nimport { resolveContext } from '../../lib/global-opts';\nimport { CliError, ExitCode, InvalidArgumentError } from '../../lib/errors';\nimport { dim, printJson, sanitizeForTerminal, sanitizeForTerminalMultiline } from '../../lib/output';\nimport { ensureStdinPiped } from '../../lib/io-helpers';\nimport pc from 'picocolors';\n\ninterface ViewFlags {\n\tfield?: string;\n}\n\n/**\n * JobViewVM 是嵌套結構(見 src/modules/jobs/view-models/job-view.vm.ts)。\n * 這裡只列我們顯示時會碰到的欄位,其他欄位走 [k: string]: unknown 保留。\n */\ninterface JobView {\n\tcompany_header_info?: {\n\t\tcompany_name?: string;\n\t\tcompany_icon_url?: string;\n\t\tenc_company_id?: string;\n\t\t[k: string]: unknown;\n\t};\n\tjob_description?: string;\n\tjob_info?: {\n\t\tjob_title?: string;\n\t\tarea_display?: string;\n\t\tsalary_display?: string;\n\t\tjob_feature_display?: string | null;\n\t\texperience_display?: string | null;\n\t\t[k: string]: unknown;\n\t};\n\tjob_information?: Record<string, unknown>;\n\trecruitment_conditions?: Record<string, unknown>;\n\tbenefits?: Record<string, unknown>;\n\tabout_company?: Record<string, unknown> | null;\n\tapplication_method?: Record<string, unknown> | null;\n\tstructured_data?: Record<string, unknown> | null;\n\t[k: string]: unknown;\n}\n\nexport function registerJobsView(parent: Command): void {\n\tparent\n\t\t.command('view <enc_id>')\n\t\t.description('View a single job. Pass \"-\" to read enc_id from stdin.')\n\t\t.option('--field <path>', 'output a single field (supports dotted paths, e.g. job_info.job_title)')\n\t\t.action(async (encIdArg: string, flags: ViewFlags, command: Command) => {\n\t\t\tconst ctx = resolveContext(command);\n\t\t\tconst encId = encIdArg === '-' ? readStdin().trim() : encIdArg;\n\t\t\tif (!encId) {\n\t\t\t\tthrow new CliError('enc_id is required', ExitCode.InvalidArgument);\n\t\t\t}\n\n\t\t\tconst client = createApiClient({\n\t\t\t\tbaseUrl: ctx.baseUrl,\n\t\t\t\tlocale: ctx.locale,\n\t\t\t\ttimeoutMs: ctx.timeoutMs,\n\t\t\t});\n\n\t\t\tconst { data, error, response } = await client.GET('/api/jobs/{encId}/view', {\n\t\t\t\tparams: { path: { encId } },\n\t\t\t});\n\t\t\tif (!response.ok) throwForHttpStatus(response.status, error);\n\n\t\t\tconst job = unwrapDataResponse<JobView>(data);\n\n\t\t\tif (flags.field) {\n\t\t\t\tconst v = getPath(job, flags.field);\n\t\t\t\tif (v === undefined) {\n\t\t\t\t\tthrow new CliError(`Field \"${flags.field}\" not present in response`, ExitCode.InvalidArgument);\n\t\t\t\t}\n\t\t\t\t// String values are employer-controlled and printed raw — sanitize. The multiline\n\t\t\t\t// variant preserves \\n (e.g. when --field selects job_description) but still strips\n\t\t\t\t// every other control char including \\r and \\t. JSON.stringify already escapes\n\t\t\t\t// < 0x20 to \\uXXXX so non-string paths don't need extra handling.\n\t\t\t\tconst out = typeof v === 'string' ? sanitizeForTerminalMultiline(v) : JSON.stringify(v, null, 2);\n\t\t\t\tprocess.stdout.write(out + '\\n');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (ctx.format === 'json') {\n\t\t\t\tprintJson(job);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trenderJobTable(job, encId, ctx.color);\n\t\t});\n}\n\nfunction renderJobTable(job: JobView, encId: string, color: boolean): void {\n\tconst label = (s: string) => (color ? pc.bold(s) : s);\n\t// All API string values are employer-controlled; sanitize before printing to defend\n\t// against terminal escape injection (clear screen, OSC 8 phishing hyperlinks, etc.).\n\tconst s = (v: string | undefined | null): string => (v ? sanitizeForTerminal(v) : '');\n\tconst info = job.job_info ?? {};\n\tconst company = job.company_header_info ?? {};\n\n\tconst lines: string[] = [];\n\tif (info.job_title) lines.push(`${label('Title:')} ${s(info.job_title)}`);\n\tif (company.company_name) lines.push(`${label('Company:')} ${s(company.company_name)}`);\n\tif (info.area_display) lines.push(`${label('Location:')} ${s(info.area_display)}`);\n\tif (info.salary_display) lines.push(`${label('Salary:')} ${s(info.salary_display)}`);\n\tif (info.job_feature_display) lines.push(`${label('Type:')} ${s(info.job_feature_display)}`);\n\tif (info.experience_display) lines.push(`${label('Experience:')} ${s(info.experience_display)}`);\n\t// encId comes from the CLI arg (user-provided) but pass through sanitize as a defense-in-depth.\n\tlines.push(dim(`enc_id: ${s(encId)}`, color));\n\tif (company.enc_company_id) lines.push(dim(`enc_company_id: ${s(company.enc_company_id)}`, color));\n\tprocess.stdout.write(lines.join('\\n') + '\\n');\n\n\tif (job.job_description) {\n\t\tprocess.stdout.write('\\n' + label('Description') + '\\n');\n\t\t// stripHtml only removes tags; sanitize afterwards in case the rich-text source\n\t\t// embedded raw escape sequences inside text nodes. Use the multiline variant so the\n\t\t// description's paragraph breaks (\\n) are preserved while \\r / \\t / other controls\n\t\t// are still stripped.\n\t\tprocess.stdout.write(renderDescription(job.job_description) + '\\n');\n\t}\n\n\tprocess.stdout.write(\n\t\t'\\n' +\n\t\t\tdim(\n\t\t\t\t'Tip: use --output json (or --field <dotted.path>, e.g. --field job_info.salary_display) for scripting.',\n\t\t\t\tcolor\n\t\t\t) +\n\t\t\t'\\n'\n\t);\n}\n\n/**\n * 後端 job_description 可能含 HTML(rich text);CLI 終端機列印時剝掉 tag。\n * 不做完整 HTML 解析 —— 只把 tag 拿掉、& 實體做最常見的還原。\n */\nfunction stripHtml(s: string): string {\n\treturn s\n\t\t.replace(/<\\/?(p|br|div|li|h[1-6])[^>]*>/gi, '\\n')\n\t\t.replace(/<[^>]+>/g, '')\n\t\t.replace(/ /g, ' ')\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/"/g, '\"')\n\t\t.replace(/'/g, \"'\")\n\t\t.replace(/\\n{3,}/g, '\\n\\n')\n\t\t.trim();\n}\n\nfunction getPath(obj: unknown, dottedPath: string): unknown {\n\tconst parts = dottedPath.split('.');\n\tlet cur: unknown = obj;\n\tfor (const p of parts) {\n\t\tif (cur && typeof cur === 'object' && Object.prototype.hasOwnProperty.call(cur, p)) {\n\t\t\tcur = (cur as Record<string, unknown>)[p];\n\t\t} else {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\treturn cur;\n}\n\nfunction renderDescription(html: string): string {\n\treturn sanitizeForTerminalMultiline(stripHtml(html));\n}\n\n// Internal helpers exposed for tests. Not part of the public CLI API.\nexport const __test__ = {\n\tstripHtml,\n\tgetPath,\n\trenderDescription,\n};\n\nfunction readStdin(): string {\n\tensureStdinPiped('view -');\n\ttry {\n\t\treturn readFileSync(0, 'utf8');\n\t} catch (err) {\n\t\tthrow new InvalidArgumentError(`Failed to read enc_id from stdin: ${(err as Error).message}`);\n\t}\n}\n","import { CliError, ExitCode, InvalidArgumentError } from './errors';\n\n/**\n * Reject with `CliError(ServerOrNetworkError)` if a promise doesn't settle within `ms`.\n *\n * Reserved for async user / network input paths that v0.2 will add (streaming jobs,\n * interactive prompts with deadlines). Existing call sites either use their own\n * mechanism (`api-client` uses `AbortSignal.timeout`, `reset.ts` uses an `'end'`\n * handler) or are synchronous (`readFileSync(0)`). Removing this until then would\n * just churn the import graph; keeping it documents the contract.\n *\n * @internal — exported for future call sites, not part of public CLI API\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst t = setTimeout(() => {\n\t\t\treject(new CliError(`Timed out after ${ms}ms: ${label}`, ExitCode.ServerOrNetworkError));\n\t\t}, ms);\n\t\tpromise.then(\n\t\t\t(v) => {\n\t\t\t\tclearTimeout(t);\n\t\t\t\tresolve(v);\n\t\t\t},\n\t\t\t(err) => {\n\t\t\t\tclearTimeout(t);\n\t\t\t\treject(err);\n\t\t\t}\n\t\t);\n\t});\n}\n\n/** Convenience: throw InvalidArgumentError on TTY stdin reads with no piped input. */\nexport function ensureStdinPiped(label: string): void {\n\tif (process.stdin.isTTY) {\n\t\tthrow new InvalidArgumentError(`${label}: no data on stdin (run via pipe, or pass the value as an arg)`);\n\t}\n}\n","import type { Command } from 'commander';\nimport { registerJobsSearch } from './search';\nimport { registerJobsView } from './view';\n\nexport function registerJobsCommand(program: Command): void {\n\tconst jobs = program.command('jobs').description('Search and view public job listings');\n\tregisterJobsSearch(jobs);\n\tregisterJobsView(jobs);\n}\n","import type { Command } from 'commander';\nimport { isConfigKey, loadConfig, saveConfig, validateAndCoerce } from '../../lib/config-store';\nimport { CliError, ExitCode } from '../../lib/errors';\n\nexport function registerConfigSet(parent: Command): void {\n\tparent\n\t\t.command('set <key> <value>')\n\t\t.description('Set a config value. Keys: locale, api_base_url, output, timeout_ms')\n\t\t.action((key: string, value: string) => {\n\t\t\tif (!isConfigKey(key)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Unknown config key \"${key}\". Allowed: locale, api_base_url, output, timeout_ms`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst coerced = validateAndCoerce(key, value);\n\t\t\tconst config = loadConfig();\n\t\t\t// Object.assign 形式避免 TS 5.5 在 `config[key] = coerced` 上推不出\n\t\t\t// ConfigValueMap[K]→CliConfig[K] 的對應;validateAndCoerce 已 generic、型別正確。\n\t\t\tObject.assign(config, { [key]: coerced });\n\t\t\tsaveConfig(config);\n\t\t\tprocess.stdout.write(`Set ${key} = ${JSON.stringify(coerced)}\\n`);\n\t\t});\n}\n","import type { Command } from 'commander';\nimport { isConfigKey, loadConfig } from '../../lib/config-store';\nimport { CliError, ExitCode } from '../../lib/errors';\nimport { printJson } from '../../lib/output';\n\nexport function registerConfigGet(parent: Command): void {\n\tparent\n\t\t.command('get [key]')\n\t\t.description('Print config value(s). With no key, prints the whole config as JSON.')\n\t\t.action((key: string | undefined) => {\n\t\t\tconst config = loadConfig();\n\t\t\tif (key === undefined) {\n\t\t\t\tprintJson(config);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!isConfigKey(key)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Unknown config key \"${key}\". Allowed: locale, api_base_url, output, timeout_ms`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst value = (config as Record<string, unknown>)[key];\n\t\t\tif (value === undefined) {\n\t\t\t\tprocess.stdout.write('\\n');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprocess.stdout.write((typeof value === 'string' ? value : JSON.stringify(value)) + '\\n');\n\t\t});\n}\n","import type { Command } from 'commander';\nimport { getConfigPath } from '../../lib/config-store';\n\nexport function registerConfigPath(parent: Command): void {\n\tparent\n\t\t.command('path')\n\t\t.description('Print the path to the config file (regardless of whether it exists)')\n\t\t.action(() => {\n\t\t\tprocess.stdout.write(getConfigPath() + '\\n');\n\t\t});\n}\n","import type { Command } from 'commander';\nimport { existsSync, unlinkSync } from 'node:fs';\nimport { getConfigPath } from '../../lib/config-store';\nimport { InvalidArgumentError } from '../../lib/errors';\n\nexport function registerConfigReset(parent: Command): void {\n\tparent\n\t\t.command('reset')\n\t\t.description('Delete the config file')\n\t\t.option('-f, --force', 'skip the confirmation prompt')\n\t\t.action(async (opts: { force?: boolean }) => {\n\t\t\tconst path = getConfigPath();\n\t\t\tif (!existsSync(path)) {\n\t\t\t\tprocess.stdout.write('No config file to delete.\\n');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!opts.force) {\n\t\t\t\tconst ok = await promptYesNo(`Delete config at ${path}? [y/N] `);\n\t\t\t\tif (!ok) {\n\t\t\t\t\tprocess.stdout.write('Aborted.\\n');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tunlinkSync(path);\n\t\t\t} catch (err) {\n\t\t\t\tconst e = err as NodeJS.ErrnoException;\n\t\t\t\tif (e.code === 'ENOENT') {\n\t\t\t\t\t// Race with another process: someone deleted it between exists & unlink.\n\t\t\t\t\tprocess.stdout.write('No config file to delete (already removed).\\n');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthrow new InvalidArgumentError(`Failed to delete config at ${path}: ${e.message}`);\n\t\t\t}\n\t\t\tprocess.stdout.write(`Deleted ${path}\\n`);\n\t\t});\n}\n\nfunction promptYesNo(prompt: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tprocess.stdout.write(prompt);\n\t\tlet buf = '';\n\t\tprocess.stdin.setEncoding('utf8');\n\t\tconst onData = (chunk: string) => {\n\t\t\tbuf += chunk;\n\t\t\tconst nl = buf.indexOf('\\n');\n\t\t\tif (nl >= 0) {\n\t\t\t\tcleanup();\n\t\t\t\tconst answer = buf.slice(0, nl).trim().toLowerCase();\n\t\t\t\tresolve(answer === 'y' || answer === 'yes');\n\t\t\t}\n\t\t};\n\t\tconst onEnd = () => {\n\t\t\tcleanup();\n\t\t\tprocess.stdout.write('\\n(no input — aborting)\\n');\n\t\t\tresolve(false);\n\t\t};\n\t\tconst cleanup = () => {\n\t\t\tprocess.stdin.removeListener('data', onData);\n\t\t\tprocess.stdin.removeListener('end', onEnd);\n\t\t\tprocess.stdin.pause();\n\t\t};\n\t\tprocess.stdin.on('data', onData);\n\t\tprocess.stdin.on('end', onEnd);\n\t});\n}\n","import type { Command } from 'commander';\nimport { registerConfigSet } from './set';\nimport { registerConfigGet } from './get';\nimport { registerConfigPath } from './path';\nimport { registerConfigReset } from './reset';\n\nexport function registerConfigCommand(program: Command): void {\n\tconst config = program.command('config').description('Manage CLI configuration');\n\tregisterConfigSet(config);\n\tregisterConfigGet(config);\n\tregisterConfigPath(config);\n\tregisterConfigReset(config);\n}\n"],"mappings":"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","names":["pc","Table","import_node_fs","createClient","envPaths","import_node_fs","import_picocolors","s","pc","program","import_node_fs","program"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/lib/errors.ts","../src/lib/output.ts","../src/commands/jobs/search.ts","../src/lib/api-client.ts","../src/lib/config-store.ts","../src/lib/global-opts.ts","../src/lib/path-utils.ts","../src/lib/concurrency.ts","../src/lib/io-helpers.ts","../src/commands/jobs/view.ts","../src/commands/jobs/index.ts","../src/commands/config/set.ts","../src/commands/config/get.ts","../src/commands/config/path.ts","../src/commands/config/reset.ts","../src/commands/config/index.ts","../src/commands/doctor.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { isCliError, ExitCode } from './lib/errors';\nimport { isColorEnabled, printError } from './lib/output';\nimport { registerJobsCommand } from './commands/jobs';\nimport { registerConfigCommand } from './commands/config';\nimport { registerDoctorCommand } from './commands/doctor';\n\nconst program = new Command();\n\nprogram\n\t.name('wport')\n\t.description('wport CLI — terminal interface to the W101 Talent Search Hub public API')\n\t.version(__CLI_VERSION__, '-v, --version', 'output the CLI version')\n\t.option('--lang <locale>', 'Accept-Language locale: zh-TW | en-US | vi-VN | th-TH | id-ID')\n\t.option('--api <url>', 'override API base URL')\n\t.option('--output <fmt>', 'output format: table | json')\n\t.option('--no-color', 'disable color output')\n\t.option('--timeout <ms>', 'HTTP timeout in milliseconds', (v) => Number(v));\n\nregisterJobsCommand(program);\nregisterConfigCommand(program);\nregisterDoctorCommand(program);\n\nprogram.exitOverride();\n\nprogram\n\t.parseAsync(process.argv)\n\t.then(() => process.exit(ExitCode.Success))\n\t.catch((err: unknown) => handleTopLevelError(err));\n\nfunction handleTopLevelError(err: unknown): never {\n\tconst color = isColorEnabled(false);\n\n\t// commander throws CommanderError on its own validation paths (help, version, unknown option).\n\tif (err && typeof err === 'object' && 'code' in err) {\n\t\tconst commanderErr = err as { code?: string; exitCode?: number; message?: string };\n\t\tif (commanderErr.code === 'commander.helpDisplayed' || commanderErr.code === 'commander.version') {\n\t\t\tprocess.exit(ExitCode.Success);\n\t\t}\n\t\t// commander 自己的 exitCode 多半是 1,不在 CLI 的 contract(0/2/3/4/5)內。\n\t\t// 任何 parsing / usage 失敗都當 InvalidArgument(2)。\n\t\tif (commanderErr.message) printError(commanderErr.message, color);\n\t\tprocess.exit(ExitCode.InvalidArgument);\n\t}\n\n\tif (isCliError(err)) {\n\t\tprintError(err.message, color);\n\t\tprocess.exit(err.exitCode);\n\t}\n\n\tconst fallbackMessage = err instanceof Error ? err.message : String(err);\n\tprintError(fallbackMessage, color);\n\tprocess.exit(ExitCode.ServerOrNetworkError);\n}\n","export const ExitCode = {\n\tSuccess: 0,\n\tInvalidArgument: 2,\n\tServerClientError: 3,\n\tServerOrNetworkError: 4,\n\tConfigCorrupt: 5,\n} as const;\n\nexport type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];\n\nexport class CliError extends Error {\n\treadonly exitCode: ExitCodeValue;\n\n\tconstructor(message: string, exitCode: ExitCodeValue) {\n\t\tsuper(message);\n\t\tthis.name = 'CliError';\n\t\tthis.exitCode = exitCode;\n\t}\n}\n\nexport class InvalidArgumentError extends CliError {\n\tconstructor(message: string) {\n\t\tsuper(message, ExitCode.InvalidArgument);\n\t\tthis.name = 'InvalidArgumentError';\n\t}\n}\n\nexport class ServerClientHttpError extends CliError {\n\treadonly status: number;\n\treadonly body: unknown;\n\tconstructor(message: string, status: number, body: unknown) {\n\t\tsuper(message, ExitCode.ServerClientError);\n\t\tthis.name = 'ServerClientHttpError';\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\nexport class NetworkError extends CliError {\n\treadonly cause?: unknown;\n\tconstructor(message: string, cause?: unknown) {\n\t\tsuper(message, ExitCode.ServerOrNetworkError);\n\t\tthis.name = 'NetworkError';\n\t\tthis.cause = cause;\n\t}\n}\n\nexport class ConfigCorruptError extends CliError {\n\treadonly path?: string;\n\tconstructor(message: string, path?: string) {\n\t\tsuper(message, ExitCode.ConfigCorrupt);\n\t\tthis.name = 'ConfigCorruptError';\n\t\tthis.path = path;\n\t}\n}\n\nexport function isCliError(err: unknown): err is CliError {\n\treturn err instanceof CliError;\n}\n","import Table from 'cli-table3';\nimport pc from 'picocolors';\nimport { CliError, ExitCode } from './errors';\n\nexport type OutputFormat = 'table' | 'json';\n\n/**\n * 把 untrusted 字串(從 API 回來的 employer-controlled 內容)變成終端機可安全列印的形式。\n *\n * 防的是 terminal escape injection:\n * - CSI / OSC / DCS 等 ESC 開頭序列(清螢幕、改 title、移動游標、假超連結 phishing 等)\n * - 其他 C0 / C1 控制字元(保留 \\t \\n \\r 三個合法格式化字元)\n *\n * JSON 模式不需要 sanitize:JSON.stringify 會把 < 0x20 的字元 escape 成 \\uXXXX。\n * 只有 table / plain-text 印到 stdout/stderr 的字串走這個 helper。\n *\n * 用 new RegExp(string) 建構,所有控制字元以 \\\\uNNNN 形式撰寫,避免 source 內含 literal 控制字元。\n */\nconst ANSI_ESCAPE_SEQUENCE = new RegExp(\n\t[\n\t\t// CSI: ESC [ params intermediates final\n\t\t'\\\\u001B\\\\[[0-?]*[ -/]*[@-~]',\n\t\t// OSC: ESC ] payload (any chars except BEL/ESC) terminated by BEL or ESC \\\n\t\t'\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n\t\t// Two-char escapes: ESC + Fe final byte (0x40-0x5F = @ A B ... Z [ \\ ] ^ _).\n\t\t// 涵蓋 CSI([) / OSC(]) / DCS(P) / SOS(X) / ST(\\\\) / PM(^) / APC(_) intro。\n\t\t// CSI / OSC regex 在前面 OR-分支會先匹配對應序列;這條兜底所有未覆蓋 Fe。\n\t\t'\\\\u001B[@-_]',\n\t].join('|'),\n\t'g'\n);\n\n// 單行用:剝所有 C0(含 \\t \\n \\r)、DEL、C1。table cell、label-prefixed 標題、\n// error / warning 訊息都走這條 —— 即便 ANSI 已剝乾淨,殘留 \\r 仍能把 cursor 拉回\n// 行首蓋掉前面的內容;\\n 會打斷表格排版、可能偽造後續 row;\\t 寬度可變、\n// 搞壞 cli-table3 對齊。\nconst CONTROL_CHARS_STRICT = new RegExp('[\\\\u0000-\\\\u001F\\\\u007F\\\\u0080-\\\\u009F]', 'g');\n\n// 多行用:內部先把 \\r\\n / lone \\r 正規化成 \\n,再剝其他 C0(含 \\t)、DEL、C1。\n// 保留 \\n 作為合法段落分隔。Caller 不需事先做正規化。\nconst CONTROL_CHARS_MULTILINE = new RegExp('[\\\\u0000-\\\\u0009\\\\u000B-\\\\u001F\\\\u007F\\\\u0080-\\\\u009F]', 'g');\n\nexport function sanitizeForTerminal(s: string): string {\n\treturn s.replace(ANSI_ESCAPE_SEQUENCE, '').replace(CONTROL_CHARS_STRICT, '');\n}\n\nexport function sanitizeForTerminalMultiline(s: string): string {\n\treturn s.replace(ANSI_ESCAPE_SEQUENCE, '').replace(/\\r\\n?/g, '\\n').replace(CONTROL_CHARS_MULTILINE, '');\n}\n\nexport function resolveOutputFormat(explicit: string | undefined): OutputFormat {\n\tif (explicit === undefined) {\n\t\treturn process.stdout.isTTY ? 'table' : 'json';\n\t}\n\tif (explicit !== 'json' && explicit !== 'table') {\n\t\tthrow new CliError(`Invalid --output \"${explicit}\". Allowed: table, json`, ExitCode.InvalidArgument);\n\t}\n\treturn explicit;\n}\n\nexport function isColorEnabled(noColor: boolean | undefined): boolean {\n\tif (noColor === true) return false;\n\tif (process.env.NO_COLOR) return false;\n\treturn process.stdout.isTTY ?? false;\n}\n\nexport function printJson(value: unknown): void {\n\tprocess.stdout.write(JSON.stringify(value, null, 2) + '\\n');\n}\n\n/**\n * Emit one newline-delimited JSON record (ND-JSON). JSON.stringify escapes control\n * chars to \\uXXXX, so employer-controlled string values are terminal-safe without\n * extra sanitization. Used by `jobs view --batch`, where one record per line keeps a\n * single failure isolated to its own line.\n */\nexport function printNdjsonLine(value: unknown): void {\n\tprocess.stdout.write(JSON.stringify(value) + '\\n');\n}\n\nexport interface TableColumn<T> {\n\theader: string;\n\tvalue: (row: T) => string;\n\tmaxWidth?: number;\n}\n\nexport function printTable<T>(rows: T[], columns: TableColumn<T>[], color: boolean): void {\n\tif (rows.length === 0) {\n\t\tprocess.stdout.write(color ? pc.dim('(no results)\\n') : '(no results)\\n');\n\t\treturn;\n\t}\n\tconst table = new Table({\n\t\thead: columns.map((c) => (color ? pc.bold(c.header) : c.header)),\n\t\tstyle: { head: [], border: [] },\n\t\tcolWidths: columns.map((c) => c.maxWidth ?? null),\n\t\twordWrap: true,\n\t});\n\tfor (const row of rows) {\n\t\t// 每個 cell 的字串都過 sanitize:防 employer-controlled 內容(title/company/area/salary 等)注入 escape。\n\t\ttable.push(columns.map((c) => sanitizeForTerminal(c.value(row))));\n\t}\n\tprocess.stdout.write(table.toString() + '\\n');\n}\n\nexport function printError(message: string, color: boolean): void {\n\tconst prefix = color ? pc.red('Error:') : 'Error:';\n\t// Server 回的 error message 也視為 untrusted,sanitize。\n\tprocess.stderr.write(`${prefix} ${sanitizeForTerminal(message)}\\n`);\n}\n\nexport function printWarn(message: string, color: boolean): void {\n\tconst prefix = color ? pc.yellow('Warning:') : 'Warning:';\n\tprocess.stderr.write(`${prefix} ${sanitizeForTerminal(message)}\\n`);\n}\n\nexport function dim(text: string, color: boolean): string {\n\treturn color ? pc.dim(text) : text;\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { asPaginatedBody, createApiClient, throwForHttpStatus } from '../../lib/api-client';\nimport { resolveContext } from '../../lib/global-opts';\nimport { CliError, ExitCode } from '../../lib/errors';\nimport { dim, printJson, printTable, printWarn } from '../../lib/output';\nimport { parseFieldsList, pickPaths } from '../../lib/path-utils';\nimport type { operations } from '../../generated/schema';\n\ninterface SearchFlags {\n\tkeyword?: string;\n\tlocation?: string[];\n\tcategory?: string[];\n\tpage?: number;\n\tpageSize?: number;\n\tjsonQuery?: string;\n\tfields?: string;\n\tminimal?: boolean;\n}\n\n/**\n * Compact field set for `--minimal` — the columns an agent almost always wants from a\n * search result, mirroring the table view minus the noise. Keeps `enc_id` first so the\n * output is directly pipeable into `jobs view -`.\n */\nconst MINIMAL_SEARCH_FIELDS = ['enc_id', 'title', 'company_name', 'area_display', 'salary_display'];\n\n/**\n * Query type derived from the generated OpenAPI schema for GET /api/jobs/search.\n * Adding a new field on the backend DTO + re-running gen:openapi will make this type\n * widen automatically; any flag we forget to map will surface as a TS error inside\n * buildQuery() instead of being silently dropped at runtime.\n */\ntype SearchQuery = NonNullable<operations['JobsController_searchJobs']['parameters']['query']>;\n\ninterface JobSearchItem {\n\tenc_id?: string;\n\tenc_company_id?: string;\n\tcompany_name?: string;\n\tcompany_logo_url?: string;\n\ttitle?: string;\n\tarea_display?: string;\n\tsalary_display?: string;\n\tsalary_currency_code?: string | null;\n\ttags?: string[];\n\tupdated_at?: string;\n\t[k: string]: unknown;\n}\n\nexport function registerJobsSearch(parent: Command): void {\n\tparent\n\t\t.command('search')\n\t\t.description(\n\t\t\t'Search public job listings. Sort is server-controlled (publish date, or relevance when --keyword is set); ' +\n\t\t\t\t'the orderBy / order query params are silently ignored, so this CLI intentionally exposes no sort flags. ' +\n\t\t\t\t'Run `wport doctor` for the full list of server quirks.'\n\t\t)\n\t\t.option('-k, --keyword <text>', 'keyword search (title / company name etc.)')\n\t\t.option('-l, --location <code...>', 'area code (repeatable, e.g. 6001001000)')\n\t\t.option('-c, --category <code...>', 'job classification code (repeatable)')\n\t\t.option('-p, --page <n>', 'page number (default 1)', (v) => Number(v))\n\t\t.option('-s, --page-size <n>', 'page size (default 10, max 100)', (v) => Number(v))\n\t\t.option('--json-query <file>', 'read full query body from JSON file (overrides other flags)')\n\t\t.option(\n\t\t\t'--fields <list>',\n\t\t\t'keep only these fields in each JSON result (comma-separated dotted paths, e.g. enc_id,title). JSON output only.'\n\t\t)\n\t\t.option('--minimal', `shorthand for --fields ${MINIMAL_SEARCH_FIELDS.join(',')}. JSON output only.`)\n\t\t.action(async (flags: SearchFlags, command: Command) => {\n\t\t\tconst ctx = resolveContext(command);\n\t\t\tconst fields = resolveSearchFields(flags);\n\t\t\tconst query = buildQuery(flags);\n\n\t\t\tconst client = createApiClient({\n\t\t\t\tbaseUrl: ctx.baseUrl,\n\t\t\t\tlocale: ctx.locale,\n\t\t\t\ttimeoutMs: ctx.timeoutMs,\n\t\t\t});\n\n\t\t\t// openapi-fetch consumes the response body itself; use `data` (success) or `error` (non-2xx).\n\t\t\tconst { data, error, response } = await client.GET('/api/jobs/search', {\n\t\t\t\tparams: { query },\n\t\t\t});\n\t\t\tif (!response.ok) throwForHttpStatus(response.status, error);\n\n\t\t\tconst paged = asPaginatedBody<JobSearchItem>(data);\n\n\t\t\tif (ctx.format === 'json') {\n\t\t\t\t// Field projection is client-side: it trims tokens the model has to read, not\n\t\t\t\t// bytes-on-the-wire (the server has no projection param). Pagination metadata is\n\t\t\t\t// preserved by spreading `paged` and only replacing `data`.\n\t\t\t\tconst body = fields ? { ...paged, data: paged.data.map((item) => pickPaths(item, fields)) } : paged;\n\t\t\t\tprintJson(body);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Table columns are fixed; --fields / --minimal are a JSON-only affordance. Warn\n\t\t\t// rather than silently ignore so the flag doesn't look broken.\n\t\t\tif (fields) {\n\t\t\t\tprintWarn('--fields / --minimal only affect JSON output; ignored for table. Use --output json.', ctx.color);\n\t\t\t}\n\n\t\t\tprintTable(\n\t\t\t\tpaged.data,\n\t\t\t\t[\n\t\t\t\t\t{ header: 'ENC_ID', value: (r) => truncate(r.enc_id ?? '', 14) },\n\t\t\t\t\t{ header: 'TITLE', value: (r) => r.title ?? '', maxWidth: 36 },\n\t\t\t\t\t{ header: 'COMPANY', value: (r) => r.company_name ?? '', maxWidth: 20 },\n\t\t\t\t\t{ header: 'LOCATION', value: (r) => r.area_display ?? '', maxWidth: 18 },\n\t\t\t\t\t{ header: 'SALARY', value: (r) => r.salary_display ?? '', maxWidth: 18 },\n\t\t\t\t\t{ header: 'UPDATED', value: (r) => formatDate(r.updated_at), maxWidth: 12 },\n\t\t\t\t],\n\t\t\t\tctx.color\n\t\t\t);\n\n\t\t\tconst head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} results).`;\n\t\t\tconst hint =\n\t\t\t\tpaged.totalPages > paged.currentPage ? ` Next: wport jobs search --page ${paged.currentPage + 1}` : '';\n\t\t\tprocess.stdout.write(dim(head + hint, ctx.color) + '\\n');\n\t\t});\n}\n\nfunction resolveSearchFields(flags: SearchFlags): string[] | undefined {\n\tif (flags.fields && flags.minimal) {\n\t\tthrow new CliError('Use either --fields or --minimal, not both', ExitCode.InvalidArgument);\n\t}\n\tif (flags.minimal) return [...MINIMAL_SEARCH_FIELDS];\n\tif (flags.fields) return parseFieldsList(flags.fields);\n\treturn undefined;\n}\n\nfunction buildQuery(flags: SearchFlags): SearchQuery {\n\tif (flags.jsonQuery) {\n\t\t// User-supplied JSON escape hatch — we trust the caller. Runtime validation will come\n\t\t// from the server side; this is the one spot the typed query is deliberately relaxed.\n\t\treturn readJsonQuery(flags.jsonQuery) as SearchQuery;\n\t}\n\tconst q: SearchQuery = {};\n\tif (flags.keyword) q.keyword = flags.keyword;\n\tif (flags.location?.length) q.area_codes = flags.location;\n\tif (flags.category?.length) q.job_classification_codes = flags.category;\n\tif (flags.page !== undefined) q.currentPage = flags.page;\n\tif (flags.pageSize !== undefined) q.pageSize = flags.pageSize;\n\treturn q;\n}\n\nfunction readJsonQuery(path: string): Record<string, unknown> {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, 'utf8');\n\t} catch (err) {\n\t\tthrow new CliError(`Cannot read --json-query file ${path}: ${(err as Error).message}`, ExitCode.InvalidArgument);\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw);\n\t\tif (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n\t\t\tthrow new Error(\n\t\t\t\tArray.isArray(parsed)\n\t\t\t\t\t? 'JSON root is an array; expected an object with query fields'\n\t\t\t\t\t: 'JSON root must be an object'\n\t\t\t);\n\t\t}\n\t\treturn parsed as Record<string, unknown>;\n\t} catch (err) {\n\t\tthrow new CliError(`Invalid JSON in ${path}: ${(err as Error).message}`, ExitCode.InvalidArgument);\n\t}\n}\n\nfunction truncate(s: string, max: number): string {\n\t// Fast path: UTF-16 code units (string.length) are always >= codepoint count,\n\t// so if byte-length already fits we don't need codepoint counting.\n\tif (s.length <= max) return s;\n\t// Array.from iterates by code-point, so surrogate pairs (emoji, supplementary\n\t// plane chars) aren't split mid-character. Doesn't handle grapheme clusters\n\t// (combining marks), but covers the common bilingual / emoji case.\n\tconst chars = Array.from(s);\n\tif (chars.length <= max) return s;\n\treturn chars.slice(0, max - 1).join('') + '…';\n}\n\nfunction formatDate(s: string | undefined): string {\n\tif (!s) return '';\n\tconst m = /^(\\d{4}-\\d{2}-\\d{2})/.exec(s);\n\treturn m ? m[1] : s;\n}\n","import createClient, { type Client } from 'openapi-fetch';\nimport type { paths } from '../generated/schema';\nimport { CliError, ExitCode, NetworkError, ServerClientHttpError } from './errors';\n\nexport interface ApiClientOptions {\n\tbaseUrl: string;\n\tlocale: string;\n\ttimeoutMs: number;\n}\n\nexport type ApiClient = Client<paths>;\n\n// Connection reuse for batch / repeated requests is handled by Node's built-in fetch\n// (undici): its global dispatcher pools keep-alive connections per origin, so multiple\n// requests from one process to the same host already reuse TCP/TLS without any setup\n// here. We deliberately don't ship a custom `undici` Agent — it'd add a runtime\n// dependency to nudge keepAliveTimeout (~4s default → longer) for near-zero gain on the\n// short-lived bursts `jobs view --batch` produces.\nexport function createApiClient(opts: ApiClientOptions): ApiClient {\n\treturn createClient<paths>({\n\t\tbaseUrl: opts.baseUrl,\n\t\theaders: {\n\t\t\t'Accept-Language': opts.locale,\n\t\t\t'User-Agent': buildUserAgent(),\n\t\t\tAccept: 'application/json',\n\t\t},\n\t\tfetch: (request: Request) => fetchWithTimeout(request, opts.timeoutMs),\n\t});\n}\n\nfunction fetchWithTimeout(request: Request, timeoutMs: number): Promise<Response> {\n\t// AbortSignal.timeout is available since Node 17.3 (engines requires >=18.17).\n\t// New Request inherits method/headers/body but overrides signal.\n\tconst timedRequest = new Request(request, { signal: AbortSignal.timeout(timeoutMs) });\n\treturn fetch(timedRequest).catch((err: unknown) => {\n\t\tif (isTimeoutAbort(err)) {\n\t\t\tthrow new NetworkError(`Request timed out after ${timeoutMs}ms`, err);\n\t\t}\n\t\t// TypeError: fetch failed → ECONNREFUSED / ENOTFOUND / TLS / proxy\n\t\tconst code = (err as { cause?: { code?: string } })?.cause?.code;\n\t\tconst detail = code ? code : ((err as Error)?.message ?? String(err));\n\t\tthrow new NetworkError(`Cannot reach upstream: ${detail}`, err);\n\t});\n}\n\nfunction isTimeoutAbort(err: unknown): boolean {\n\tif (err && typeof err === 'object' && 'name' in err) {\n\t\tconst name = (err as { name?: string }).name;\n\t\treturn name === 'TimeoutError' || name === 'AbortError';\n\t}\n\treturn false;\n}\n\nexport function buildUserAgent(): string {\n\treturn `wport-cli/${__CLI_VERSION__} (node ${process.version}; ${process.platform})`;\n}\n\n/**\n * W101 後端 `DataResponse<T>` wrapper:`{ success, statusCode, message, data: T }`。\n * 用於回傳「單筆」payload 的端點(例如 GET /api/jobs/:encId/view)。\n *\n * Shape 不符直接 throw CliError,不再 silent cast 把 wrapper 當 payload 回。\n *\n * 注意:`PaginatedResponse` 是另一種扁平結構\n * `{ success, statusCode, message, data: T[], currentPage, totalPages, pageSize, totalCount }`,\n * 不要對 paginated 回應用此函式 —— 會丟失 pagination 欄位。\n */\nexport function unwrapDataResponse<T>(body: unknown): T {\n\tif (body && typeof body === 'object' && 'success' in body && 'data' in body) {\n\t\treturn (body as { data: T }).data;\n\t}\n\tthrow new CliError('Unexpected response shape: missing { success, data } wrapper', ExitCode.ServerOrNetworkError);\n}\n\n/**\n * W101 後端 `PaginatedResponse<T>` wrapper(扁平):\n * `{ success, statusCode, message, data: T[], currentPage, totalPages, pageSize, totalCount }`。\n * 直接 cast,不剝外層;callers 從 `.data` 拿 list,從頂層欄位拿分頁 metadata。\n */\nexport interface PaginatedBody<T> {\n\tsuccess?: boolean;\n\tstatusCode?: number;\n\tmessage?: unknown;\n\tdata: T[];\n\tcurrentPage: number;\n\ttotalPages: number;\n\tpageSize: number;\n\ttotalCount: number;\n}\n\nexport function asPaginatedBody<T>(body: unknown): PaginatedBody<T> {\n\tif (!body || typeof body !== 'object') {\n\t\tthrow new CliError('Unexpected response shape: not an object', ExitCode.ServerOrNetworkError);\n\t}\n\tconst b = body as Record<string, unknown>;\n\tif (!Array.isArray(b.data)) {\n\t\tthrow new CliError('Unexpected response shape: missing `data` array', ExitCode.ServerOrNetworkError);\n\t}\n\tfor (const key of ['currentPage', 'totalPages', 'pageSize', 'totalCount'] as const) {\n\t\tif (typeof b[key] !== 'number') {\n\t\t\tthrow new CliError(`Unexpected response shape: missing or non-numeric \"${key}\"`, ExitCode.ServerOrNetworkError);\n\t\t}\n\t}\n\treturn body as PaginatedBody<T>;\n}\n\nexport function throwForHttpStatus(status: number, body: unknown): never {\n\tconst message = extractErrorMessage(body) ?? `HTTP ${status}`;\n\tif (status >= 400 && status < 500) {\n\t\tthrow new ServerClientHttpError(message, status, body);\n\t}\n\tthrow new CliError(message, ExitCode.ServerOrNetworkError);\n}\n\nfunction extractErrorMessage(body: unknown): string | null {\n\tif (typeof body === 'string') return body;\n\tif (body && typeof body === 'object') {\n\t\tconst obj = body as Record<string, unknown>;\n\t\t// NestJS class-validator default: message: string[]\n\t\tif (Array.isArray(obj.message)) {\n\t\t\tconst parts = obj.message.filter((m): m is string => typeof m === 'string');\n\t\t\tif (parts.length > 0) return parts.join('; ');\n\t\t}\n\t\tif (typeof obj.message === 'string') return obj.message;\n\t\t// i18n object 或 nested → 印 JSON tail\n\t\tif (obj.message && typeof obj.message === 'object') {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(obj.message);\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t}\n\t\tif (typeof obj.error === 'string') return obj.error;\n\t}\n\treturn null;\n}\n","import {\n\texistsSync,\n\tmkdirSync,\n\treadFileSync,\n\tchmodSync,\n\topenSync,\n\twriteSync,\n\tcloseSync,\n\trenameSync,\n\tunlinkSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport envPaths from 'env-paths';\nimport { CliError, ExitCode } from './errors';\nimport { printWarn } from './output';\n\nexport const ALLOWED_LOCALES = ['zh-TW', 'en-US', 'vi-VN', 'th-TH', 'id-ID'] as const;\nexport type Locale = (typeof ALLOWED_LOCALES)[number];\n\nexport const ALLOWED_OUTPUT = ['table', 'json'] as const;\nexport type OutputPref = (typeof ALLOWED_OUTPUT)[number];\n\nexport interface CliConfig {\n\tlocale?: Locale;\n\toutput?: OutputPref;\n\ttimeout_ms?: number;\n}\n\nconst CONFIG_KEYS = ['locale', 'output', 'timeout_ms'] as const satisfies readonly (keyof CliConfig)[];\nexport type ConfigKey = (typeof CONFIG_KEYS)[number];\n\n/**\n * Keys recognised in older configs but no longer settable. Detected in parseConfig\n * to surface a one-time deprecation warning instead of silently dropping (which the\n * forward-compat path would do for genuinely unknown keys).\n *\n * `api_base_url` was removed in 0.1.2 — SSRF / credential exfil surface for a flag\n * external users don't actually need. Override via `WPORT_API_BASE` env var or `--api`.\n */\nconst DEPRECATED_CONFIG_KEYS = ['api_base_url'] as const;\ntype DeprecatedConfigKey = (typeof DEPRECATED_CONFIG_KEYS)[number];\n\n/** Per-key migration hint shown when a deprecated key is found in an existing config. */\nconst DEPRECATED_KEY_HINTS: Record<DeprecatedConfigKey, string> = {\n\tapi_base_url: 'Set the WPORT_API_BASE env var (or use --api) instead.',\n};\n\n// One-time latch so repeated loadConfig() calls (e.g. inside a batch run) emit the\n// deprecation notice at most once per process rather than spamming stderr.\nlet deprecationWarned = false;\n\n/** Per-key value type. validateAndCoerce<K> returns ConfigValueMap[K]. */\ntype ConfigValueMap = {\n\tlocale: Locale;\n\toutput: OutputPref;\n\ttimeout_ms: number;\n};\n\nexport function isConfigKey(key: string): key is ConfigKey {\n\treturn (CONFIG_KEYS as readonly string[]).includes(key);\n}\n\nexport function isDeprecatedConfigKey(key: string): key is DeprecatedConfigKey {\n\treturn (DEPRECATED_CONFIG_KEYS as readonly string[]).includes(key);\n}\n\nconst paths = envPaths('wport', { suffix: '' });\n\nexport function getConfigPath(): string {\n\treturn join(paths.config, 'config.json');\n}\n\n/**\n * Trust boundary:把外部 JSON object 收成型別正確的 CliConfig。\n * 每個 key 都走 validateAndCoerce,不認識的 key 直接 drop(forward compatible,\n * 未來新版多塞了 key、舊 CLI 不會炸)。\n */\nexport function parseConfig(raw: unknown): CliConfig {\n\tif (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n\t\tthrow new CliError('Config must be a JSON object', ExitCode.ConfigCorrupt);\n\t}\n\tconst input = raw as Record<string, unknown>;\n\n\t// Surface (once per process) any deprecated key still sitting in the user's config.\n\t// We warn + drop rather than error, so upgrading from <=0.1.1 never hard-fails; the\n\t// key's actual replacement (WPORT_API_BASE) is resolved elsewhere in global-opts.\n\tif (!deprecationWarned) {\n\t\tfor (const dep of DEPRECATED_CONFIG_KEYS) {\n\t\t\tif (dep in input) {\n\t\t\t\tdeprecationWarned = true;\n\t\t\t\tprintWarn(`Config key \"${dep}\" was removed in 0.1.2 and is ignored. ${DEPRECATED_KEY_HINTS[dep]}`, false);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst out: CliConfig = {};\n\tfor (const key of CONFIG_KEYS) {\n\t\tif (!(key in input)) continue;\n\t\tconst value = input[key];\n\t\ttry {\n\t\t\tconst coerced = validateAndCoerce(key, String(value));\n\t\t\t// Object.assign 形式避免 TS 5.5 在 `out[key] = coerced` 上推不出 ConfigValueMap[K]→CliConfig[K]\n\t\t\t// 的對應關係。validateAndCoerce 已是 generic、型別正確;此處只是 indexed assignment 的繞道。\n\t\t\tObject.assign(out, { [key]: coerced });\n\t\t} catch (err) {\n\t\t\tif (err instanceof CliError) {\n\t\t\t\tthrow new CliError(`Config key \"${key}\" invalid: ${err.message}`, ExitCode.ConfigCorrupt);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn out;\n}\n\nexport function loadConfig(): CliConfig {\n\tconst path = getConfigPath();\n\tif (!existsSync(path)) return {};\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, 'utf8');\n\t} catch (err) {\n\t\tthrow new CliError(`Failed to read config at ${path}: ${(err as Error).message}`, ExitCode.ConfigCorrupt);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (err) {\n\t\tthrow new CliError(`Failed to parse JSON at ${path}: ${(err as Error).message}`, ExitCode.ConfigCorrupt);\n\t}\n\treturn parseConfig(parsed);\n}\n\nexport function saveConfig(config: CliConfig): void {\n\tconst path = getConfigPath();\n\tmkdirSync(dirname(path), { recursive: true });\n\n\t// Atomic: write tmpfile (mode 0o600 from open()) → rename to final path (POSIX atomic).\n\t// 即使中途 crash,舊檔仍完整、不會留 truncated JSON 觸發 ConfigCorrupt 鎖死 CLI。\n\tconst tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;\n\tconst fd = openSync(tmpPath, 'w', 0o600);\n\ttry {\n\t\twriteSync(fd, JSON.stringify(config, null, 2) + '\\n');\n\t} catch (err) {\n\t\tcloseSync(fd);\n\t\ttry {\n\t\t\tunlinkSync(tmpPath);\n\t\t} catch {\n\t\t\t/* best effort cleanup */\n\t\t}\n\t\tthrow err;\n\t}\n\tcloseSync(fd);\n\n\t// POSIX: openSync's mode is masked by umask (umask can only narrow, never widen).\n\t// chmod restores 0o600 in case a permissive umask stripped owner bits; it cannot\n\t// expose the file to group/other. On Windows chmodSync is a no-op, skip entirely.\n\tif (process.platform !== 'win32') {\n\t\ttry {\n\t\t\tchmodSync(tmpPath, 0o600);\n\t\t} catch (err) {\n\t\t\tprintWarn(\n\t\t\t\t`Failed to chmod 0600 on config tmpfile: ${(err as Error).message}. ` +\n\t\t\t\t\t'Other users on this system may be able to read CLI config.',\n\t\t\t\tfalse\n\t\t\t);\n\t\t}\n\t}\n\n\trenameSync(tmpPath, path);\n}\n\nexport function validateAndCoerce<K extends ConfigKey>(key: K, value: string): ConfigValueMap[K] {\n\tswitch (key) {\n\t\tcase 'locale': {\n\t\t\tif (!(ALLOWED_LOCALES as readonly string[]).includes(value)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Invalid locale \"${value}\". Allowed: ${ALLOWED_LOCALES.join(', ')}`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn value as ConfigValueMap[K];\n\t\t}\n\t\tcase 'output': {\n\t\t\tif (!(ALLOWED_OUTPUT as readonly string[]).includes(value)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Invalid output \"${value}\". Allowed: ${ALLOWED_OUTPUT.join(', ')}`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn value as ConfigValueMap[K];\n\t\t}\n\t\tcase 'timeout_ms': {\n\t\t\tconst n = Number(value);\n\t\t\tif (!Number.isInteger(n) || n < 100 || n > 600_000) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`timeout_ms must be an integer between 100 and 600000 (got ${value})`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn n as ConfigValueMap[K];\n\t\t}\n\t}\n}\n\n// Internal helpers exposed for tests. Not part of the public CLI API.\nexport const __test__ = {\n\tresetDeprecationWarning(): void {\n\t\tdeprecationWarned = false;\n\t},\n};\n","import type { Command } from 'commander';\nimport { ALLOWED_LOCALES, loadConfig, type Locale, type CliConfig } from './config-store';\nimport { CliError, ExitCode } from './errors';\nimport { resolveOutputFormat, isColorEnabled, type OutputFormat } from './output';\n\n// Production public API. Local development overrides via the WPORT_API_BASE env var\n// or the `--api` flag. (The `api_base_url` config key was removed in 0.1.2 — keeping a\n// persisted, mutable base URL on disk is an SSRF / credential-exfil surface that\n// external users don't need.)\nconst DEFAULT_BASE_URL = 'https://api.wport.me';\nexport const API_BASE_ENV_VAR = 'WPORT_API_BASE';\nconst DEFAULT_LOCALE: Locale = 'zh-TW';\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\nexport interface ResolvedContext {\n\tbaseUrl: string;\n\tlocale: Locale;\n\ttimeoutMs: number;\n\tformat: OutputFormat;\n\tcolor: boolean;\n\tconfig: CliConfig;\n}\n\ninterface RawGlobals {\n\tlang?: string;\n\tapi?: string;\n\toutput?: string;\n\tcolor?: boolean;\n\ttimeout?: number;\n}\n\nexport function resolveContext(command: Command): ResolvedContext {\n\tconst globals = command.optsWithGlobals() as RawGlobals;\n\tconst config = loadConfig();\n\n\treturn {\n\t\tbaseUrl: resolveBaseUrl(globals.api),\n\t\tlocale: resolveLocale(globals.lang, config),\n\t\ttimeoutMs: resolveTimeout(globals.timeout, config),\n\t\tformat: resolveOutputFormat(globals.output),\n\t\tcolor: isColorEnabled(globals.color === false),\n\t\tconfig,\n\t};\n}\n\n/**\n * Resolve the API base URL. Precedence: `--api` flag > WPORT_API_BASE env var > default.\n * The env var is just as untrusted as the (removed) config key, so it gets the same\n * http(s)-only validation to keep the SSRF surface closed.\n */\nfunction resolveBaseUrl(override: string | undefined): string {\n\tconst fromEnv = process.env[API_BASE_ENV_VAR]?.trim();\n\tif (override !== undefined) return validateBaseUrl(override, '--api');\n\tif (fromEnv) return validateBaseUrl(fromEnv, `${API_BASE_ENV_VAR} env var`);\n\treturn DEFAULT_BASE_URL;\n}\n\nfunction validateBaseUrl(raw: string, source: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(raw);\n\t} catch {\n\t\tthrow new CliError(`Invalid API base URL from ${source}: ${raw}`, ExitCode.InvalidArgument);\n\t}\n\tif (url.protocol !== 'https:' && url.protocol !== 'http:') {\n\t\tthrow new CliError(\n\t\t\t`API base URL from ${source} must be http or https (got ${url.protocol})`,\n\t\t\tExitCode.InvalidArgument\n\t\t);\n\t}\n\treturn raw.replace(/\\/$/, '');\n}\n\nfunction resolveLocale(override: string | undefined, config: CliConfig): Locale {\n\tconst raw = override ?? config.locale ?? DEFAULT_LOCALE;\n\tif (!(ALLOWED_LOCALES as readonly string[]).includes(raw)) {\n\t\tthrow new CliError(`Invalid --lang \"${raw}\". Allowed: ${ALLOWED_LOCALES.join(', ')}`, ExitCode.InvalidArgument);\n\t}\n\treturn raw as Locale;\n}\n\nfunction resolveTimeout(override: number | undefined, config: CliConfig): number {\n\tconst raw = override ?? config.timeout_ms ?? DEFAULT_TIMEOUT_MS;\n\tif (!Number.isInteger(raw) || raw < 100 || raw > 600_000) {\n\t\tthrow new CliError(`Invalid --timeout ${raw} (must be integer 100..600000)`, ExitCode.InvalidArgument);\n\t}\n\treturn raw;\n}\n\n// Internal helpers exposed for tests. Not part of the public CLI API.\nexport const __test__ = {\n\tresolveBaseUrl,\n\tDEFAULT_BASE_URL,\n};\n","import { CliError, ExitCode } from './errors';\n\n/**\n * Read a value out of a nested object by dotted path (e.g. `job_info.job_title`).\n * Returns undefined if any segment is missing.\n *\n * Uses hasOwnProperty (not the `in` operator / direct index) so a path segment can\n * never traverse into `__proto__` / `constructor` and walk the prototype chain —\n * the input objects are server-controlled, so this is a deliberate safety boundary.\n */\nexport function getPath(obj: unknown, dottedPath: string): unknown {\n\tconst parts = dottedPath.split('.');\n\tlet cur: unknown = obj;\n\tfor (const p of parts) {\n\t\tif (cur && typeof cur === 'object' && Object.prototype.hasOwnProperty.call(cur, p)) {\n\t\t\tcur = (cur as Record<string, unknown>)[p];\n\t\t} else {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\treturn cur;\n}\n\n/**\n * Project an object down to a set of dotted paths, keyed by the path string itself\n * (so `pickPaths(job, ['job_info.job_title'])` → `{ 'job_info.job_title': '...' }`).\n *\n * Every requested path becomes a key so the shape is predictable across a list of\n * heterogeneous items: a missing path yields `null` rather than being dropped, which\n * keeps each row in `jobs search --fields` structurally identical for downstream tools.\n */\nexport function pickPaths(obj: unknown, paths: string[]): Record<string, unknown> {\n\tconst out: Record<string, unknown> = {};\n\tfor (const p of paths) {\n\t\tconst v = getPath(obj, p);\n\t\tout[p] = v === undefined ? null : v;\n\t}\n\treturn out;\n}\n\n/**\n * Parse a comma-separated `--fields` value into a trimmed, non-empty list.\n * Throws InvalidArgument if the result is empty (e.g. `--fields ,,`).\n */\nexport function parseFieldsList(raw: string): string[] {\n\tconst fields = raw\n\t\t.split(',')\n\t\t.map((s) => s.trim())\n\t\t.filter(Boolean);\n\tif (fields.length === 0) {\n\t\tthrow new CliError('--fields requires at least one field name', ExitCode.InvalidArgument);\n\t}\n\treturn fields;\n}\n","/**\n * Run `fn` over `items` with at most `limit` in flight at once, returning results in\n * the SAME order as the input (not completion order) so callers can correlate output\n * rows back to their input without threading an index through.\n *\n * `fn` is expected to handle its own errors (e.g. resolve to an error-shaped result);\n * a rejection from `fn` will reject the whole batch, so the batch caller wraps each\n * unit in try/catch to keep one failure from sinking the rest.\n */\nexport async function mapWithConcurrency<T, R>(\n\titems: readonly T[],\n\tlimit: number,\n\tfn: (item: T, index: number) => Promise<R>\n): Promise<R[]> {\n\tconst results = new Array<R>(items.length);\n\tlet cursor = 0;\n\n\tasync function worker(): Promise<void> {\n\t\tfor (;;) {\n\t\t\tconst index = cursor++;\n\t\t\tif (index >= items.length) return;\n\t\t\tresults[index] = await fn(items[index], index);\n\t\t}\n\t}\n\n\tconst workerCount = Math.min(Math.max(1, limit), items.length);\n\tawait Promise.all(Array.from({ length: workerCount }, () => worker()));\n\treturn results;\n}\n","import { readSync } from 'node:fs';\nimport { CliError, ExitCode, InvalidArgumentError } from './errors';\n\n/**\n * Reject with `CliError(ServerOrNetworkError)` if a promise doesn't settle within `ms`.\n *\n * Reserved for async user / network input paths that v0.2 will add (streaming jobs,\n * interactive prompts with deadlines). Existing call sites either use their own\n * mechanism (`api-client` uses `AbortSignal.timeout`, `reset.ts` uses an `'end'`\n * handler) or are synchronous (`readFileSync(0)`). Removing this until then would\n * just churn the import graph; keeping it documents the contract.\n *\n * @internal — exported for future call sites, not part of public CLI API\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n\treturn new Promise<T>((resolve, reject) => {\n\t\tconst t = setTimeout(() => {\n\t\t\treject(new CliError(`Timed out after ${ms}ms: ${label}`, ExitCode.ServerOrNetworkError));\n\t\t}, ms);\n\t\tpromise.then(\n\t\t\t(v) => {\n\t\t\t\tclearTimeout(t);\n\t\t\t\tresolve(v);\n\t\t\t},\n\t\t\t(err) => {\n\t\t\t\tclearTimeout(t);\n\t\t\t\treject(err);\n\t\t\t}\n\t\t);\n\t});\n}\n\n/** Convenience: throw InvalidArgumentError on TTY stdin reads with no piped input. */\nexport function ensureStdinPiped(label: string): void {\n\tif (process.stdin.isTTY) {\n\t\tthrow new InvalidArgumentError(`${label}: no data on stdin (run via pipe, or pass the value as an arg)`);\n\t}\n}\n\n/** Standalone backstop when a caller can't supply a `--timeout`-derived bound. */\nconst DEFAULT_STDIN_TIMEOUT_MS = 30_000;\n\nexport interface ReadPipedStdinOptions {\n\t/**\n\t * Upper bound (ms) on total time spent waiting for a slow / hung upstream pipe.\n\t * Callers pass the resolved `--timeout` so the limit is user-tunable; falls back to\n\t * DEFAULT_STDIN_TIMEOUT_MS when omitted.\n\t */\n\ttimeoutMs?: number;\n\t/** Injectable clock for tests; defaults to `Date.now`. */\n\tnow?: () => number;\n}\n\n/**\n * Synchronously drain piped stdin to a string, tolerating EAGAIN.\n *\n * A single `readFileSync(0)` / `readSync` can throw EAGAIN when stdin is a non-blocking\n * pipe whose upstream process is still producing (e.g. `wport jobs search ... | jq ... |\n * wport jobs view - --batch`): the fd has no data *right now* but isn't at EOF either.\n * Naively letting that throw makes the documented pipe workflow fail intermittently. We\n * retry on EAGAIN with a ~1ms synchronous sleep (Atomics.wait, to avoid a hot spin) and\n * stop on a zero-byte read or EOF.\n *\n * If the upstream neither produces data nor closes the pipe, the EAGAIN retry would spin\n * forever (the global `--timeout` only bounds HTTP, not stdin). We cap the total wait with\n * `timeoutMs` and surface a timeout as InvalidArgumentError rather than hanging silently.\n */\nexport function readPipedStdin(label: string, options: ReadPipedStdinOptions = {}): string {\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_STDIN_TIMEOUT_MS;\n\tconst now = options.now ?? Date.now;\n\tensureStdinPiped(label);\n\tconst chunks: Buffer[] = [];\n\tconst buf = Buffer.alloc(64 * 1024);\n\tconst sleeper = new Int32Array(new SharedArrayBuffer(4));\n\tconst deadline = now() + timeoutMs;\n\tfor (;;) {\n\t\tlet bytesRead: number;\n\t\ttry {\n\t\t\tbytesRead = readSync(0, buf, 0, buf.length, null);\n\t\t} catch (err) {\n\t\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\t\tif (code === 'EAGAIN') {\n\t\t\t\tif (now() > deadline) {\n\t\t\t\t\tthrow new InvalidArgumentError(`${label}: timed out after ${timeoutMs}ms waiting for piped stdin`);\n\t\t\t\t}\n\t\t\t\tAtomics.wait(sleeper, 0, 0, 1); // sleep ~1ms, then retry\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (code === 'EOF') break;\n\t\t\tthrow new InvalidArgumentError(`${label}: failed to read stdin: ${(err as Error).message}`);\n\t\t}\n\t\tif (bytesRead === 0) break;\n\t\tchunks.push(Buffer.from(buf.subarray(0, bytesRead)));\n\t}\n\treturn Buffer.concat(chunks).toString('utf8');\n}\n","import type { Command } from 'commander';\nimport { createApiClient, throwForHttpStatus, unwrapDataResponse, type ApiClient } from '../../lib/api-client';\nimport { resolveContext } from '../../lib/global-opts';\nimport { CliError, ExitCode } from '../../lib/errors';\nimport { dim, printJson, printNdjsonLine, sanitizeForTerminal, sanitizeForTerminalMultiline } from '../../lib/output';\nimport { getPath, parseFieldsList, pickPaths } from '../../lib/path-utils';\nimport { mapWithConcurrency } from '../../lib/concurrency';\nimport { readPipedStdin } from '../../lib/io-helpers';\nimport pc from 'picocolors';\n\ninterface ViewFlags {\n\tfield?: string;\n\tfields?: string;\n\tbatch?: boolean;\n\tconcurrency?: number;\n}\n\nconst DEFAULT_BATCH_CONCURRENCY = 5;\nconst MAX_BATCH_CONCURRENCY = 20;\n\n/** One ND-JSON record emitted per enc_id in --batch mode. */\ninterface BatchResult {\n\tenc_id: string;\n\tok: boolean;\n\tdata?: unknown;\n\terror?: string;\n}\n\n/** Projects a fetched job down to whatever --field / --fields asked for (or the whole job). */\ntype JobProjector = (job: JobView) => unknown;\n\n/**\n * JobViewVM 是嵌套結構(見 src/modules/jobs/view-models/job-view.vm.ts)。\n * 這裡只列我們顯示時會碰到的欄位,其他欄位走 [k: string]: unknown 保留。\n */\ninterface JobView {\n\tcompany_header_info?: {\n\t\tcompany_name?: string;\n\t\tcompany_icon_url?: string;\n\t\tenc_company_id?: string;\n\t\t[k: string]: unknown;\n\t};\n\tjob_description?: string;\n\tjob_info?: {\n\t\tjob_title?: string;\n\t\tarea_display?: string;\n\t\tsalary_display?: string;\n\t\tjob_feature_display?: string | null;\n\t\texperience_display?: string | null;\n\t\t[k: string]: unknown;\n\t};\n\tjob_information?: Record<string, unknown>;\n\trecruitment_conditions?: Record<string, unknown>;\n\tbenefits?: Record<string, unknown>;\n\tabout_company?: Record<string, unknown> | null;\n\tapplication_method?: Record<string, unknown> | null;\n\tstructured_data?: Record<string, unknown> | null;\n\t[k: string]: unknown;\n}\n\nexport function registerJobsView(parent: Command): void {\n\tparent\n\t\t.command('view <enc_id>')\n\t\t.description('View a single job. Pass \"-\" to read enc_id from stdin.')\n\t\t.option('--field <path>', 'output a single field as a raw value (dotted paths, e.g. job_info.job_title)')\n\t\t.option(\n\t\t\t'--fields <list>',\n\t\t\t'output selected fields as a JSON object (comma-separated dotted paths, e.g. job_info.job_title,company_header_info.company_name)'\n\t\t)\n\t\t.option(\n\t\t\t'--batch',\n\t\t\t'read newline-separated enc_ids from stdin and emit one ND-JSON record per job (requires \"-\" as the enc_id arg)'\n\t\t)\n\t\t.option(\n\t\t\t'--concurrency <n>',\n\t\t\t`max parallel requests in --batch mode (default ${DEFAULT_BATCH_CONCURRENCY}, max ${MAX_BATCH_CONCURRENCY})`,\n\t\t\t(v) => Number(v)\n\t\t)\n\t\t.action(async (encIdArg: string, flags: ViewFlags, command: Command) => {\n\t\t\tconst ctx = resolveContext(command);\n\t\t\tif (flags.field && flags.fields) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t'Use either --field (single raw value) or --fields (JSON object), not both',\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst client = createApiClient({\n\t\t\t\tbaseUrl: ctx.baseUrl,\n\t\t\t\tlocale: ctx.locale,\n\t\t\t\ttimeoutMs: ctx.timeoutMs,\n\t\t\t});\n\n\t\t\tif (flags.batch) {\n\t\t\t\tawait runBatchView(encIdArg, flags, client, ctx.timeoutMs);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst encId = encIdArg === '-' ? readPipedStdin('view -', { timeoutMs: ctx.timeoutMs }).trim() : encIdArg;\n\t\t\tif (!encId) {\n\t\t\t\tthrow new CliError('enc_id is required', ExitCode.InvalidArgument);\n\t\t\t}\n\n\t\t\tconst { data, error, response } = await client.GET('/api/jobs/{encId}/view', {\n\t\t\t\tparams: { path: { encId } },\n\t\t\t});\n\t\t\tif (!response.ok) throwForHttpStatus(response.status, error);\n\n\t\t\tconst job = unwrapDataResponse<JobView>(data);\n\n\t\t\tif (flags.fields) {\n\t\t\t\t// Multi-field projection → JSON object keyed by dotted path. printJson uses\n\t\t\t\t// JSON.stringify, which escapes control chars to \\uXXXX, so employer-controlled\n\t\t\t\t// string values are safe without extra sanitization (same rationale as the\n\t\t\t\t// json branch below).\n\t\t\t\tprintJson(pickPaths(job, parseFieldsList(flags.fields)));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (flags.field) {\n\t\t\t\tconst v = getPath(job, flags.field);\n\t\t\t\tif (v === undefined) {\n\t\t\t\t\tthrow new CliError(`Field \"${flags.field}\" not present in response`, ExitCode.InvalidArgument);\n\t\t\t\t}\n\t\t\t\t// String values are employer-controlled and printed raw — sanitize. The multiline\n\t\t\t\t// variant preserves \\n (e.g. when --field selects job_description) but still strips\n\t\t\t\t// every other control char including \\r and \\t. JSON.stringify already escapes\n\t\t\t\t// < 0x20 to \\uXXXX so non-string paths don't need extra handling.\n\t\t\t\tconst out = typeof v === 'string' ? sanitizeForTerminalMultiline(v) : JSON.stringify(v, null, 2);\n\t\t\t\tprocess.stdout.write(out + '\\n');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (ctx.format === 'json') {\n\t\t\t\tprintJson(job);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trenderJobTable(job, encId, ctx.color);\n\t\t});\n}\n\n/**\n * --batch orchestration: read enc_ids off stdin, fetch each (bounded parallelism),\n * and stream one ND-JSON record per job. One failed enc_id becomes an `ok:false` line\n * rather than aborting the whole run. Output order matches input order.\n */\nasync function runBatchView(encIdArg: string, flags: ViewFlags, client: ApiClient, timeoutMs: number): Promise<void> {\n\tif (encIdArg !== '-') {\n\t\tthrow new CliError('--batch reads enc_ids from stdin; pass \"-\" as the enc_id argument', ExitCode.InvalidArgument);\n\t}\n\tconst encIds = parseBatchInput(readPipedStdin('view - --batch', { timeoutMs }));\n\tif (encIds.length === 0) {\n\t\tthrow new CliError('No enc_ids found on stdin', ExitCode.InvalidArgument);\n\t}\n\tconst concurrency = resolveBatchConcurrency(flags.concurrency);\n\tconst project = makeBatchProjector(flags);\n\tconst results = await runBatch(encIds, concurrency, (encId) => fetchJob(client, encId), project);\n\tfor (const record of results) printNdjsonLine(record);\n}\n\nasync function fetchJob(client: ApiClient, encId: string): Promise<JobView> {\n\tconst { data, error, response } = await client.GET('/api/jobs/{encId}/view', {\n\t\tparams: { path: { encId } },\n\t});\n\tif (!response.ok) throwForHttpStatus(response.status, error);\n\treturn unwrapDataResponse<JobView>(data);\n}\n\n/**\n * Pure batch driver (injectable fetcher) so the success/failure-mix behaviour is\n * testable without a live API. Each unit is wrapped so a rejected fetch turns into an\n * `ok:false` record instead of rejecting the whole batch.\n */\nasync function runBatch(\n\tencIds: string[],\n\tconcurrency: number,\n\tfetchOne: (encId: string) => Promise<JobView>,\n\tproject: JobProjector\n): Promise<BatchResult[]> {\n\treturn mapWithConcurrency(encIds, concurrency, async (encId): Promise<BatchResult> => {\n\t\ttry {\n\t\t\tconst job = await fetchOne(encId);\n\t\t\treturn { enc_id: encId, ok: true, data: project(job) };\n\t\t} catch (err) {\n\t\t\treturn { enc_id: encId, ok: false, error: err instanceof Error ? err.message : String(err) };\n\t\t}\n\t});\n}\n\nfunction makeBatchProjector(flags: ViewFlags): JobProjector {\n\tif (flags.fields) {\n\t\tconst paths = parseFieldsList(flags.fields);\n\t\treturn (job) => pickPaths(job, paths);\n\t}\n\tif (flags.field) {\n\t\tconst path = flags.field;\n\t\t// Missing path → null (keeps every record's shape stable across the batch),\n\t\t// unlike single-view --field which errors on a missing path.\n\t\treturn (job) => getPath(job, path) ?? null;\n\t}\n\treturn (job) => job;\n}\n\nfunction parseBatchInput(raw: string): string[] {\n\treturn raw\n\t\t.split('\\n')\n\t\t.map((s) => s.trim())\n\t\t.filter(Boolean);\n}\n\nfunction resolveBatchConcurrency(raw: number | undefined): number {\n\tconst n = raw ?? DEFAULT_BATCH_CONCURRENCY;\n\tif (!Number.isInteger(n) || n < 1 || n > MAX_BATCH_CONCURRENCY) {\n\t\tthrow new CliError(\n\t\t\t`--concurrency must be an integer between 1 and ${MAX_BATCH_CONCURRENCY} (got ${raw})`,\n\t\t\tExitCode.InvalidArgument\n\t\t);\n\t}\n\treturn n;\n}\n\nfunction renderJobTable(job: JobView, encId: string, color: boolean): void {\n\tconst label = (s: string) => (color ? pc.bold(s) : s);\n\t// All API string values are employer-controlled; sanitize before printing to defend\n\t// against terminal escape injection (clear screen, OSC 8 phishing hyperlinks, etc.).\n\tconst s = (v: string | undefined | null): string => (v ? sanitizeForTerminal(v) : '');\n\tconst info = job.job_info ?? {};\n\tconst company = job.company_header_info ?? {};\n\n\tconst lines: string[] = [];\n\tif (info.job_title) lines.push(`${label('Title:')} ${s(info.job_title)}`);\n\tif (company.company_name) lines.push(`${label('Company:')} ${s(company.company_name)}`);\n\tif (info.area_display) lines.push(`${label('Location:')} ${s(info.area_display)}`);\n\tif (info.salary_display) lines.push(`${label('Salary:')} ${s(info.salary_display)}`);\n\tif (info.job_feature_display) lines.push(`${label('Type:')} ${s(info.job_feature_display)}`);\n\tif (info.experience_display) lines.push(`${label('Experience:')} ${s(info.experience_display)}`);\n\t// encId comes from the CLI arg (user-provided) but pass through sanitize as a defense-in-depth.\n\tlines.push(dim(`enc_id: ${s(encId)}`, color));\n\tif (company.enc_company_id) lines.push(dim(`enc_company_id: ${s(company.enc_company_id)}`, color));\n\tprocess.stdout.write(lines.join('\\n') + '\\n');\n\n\tif (job.job_description) {\n\t\tprocess.stdout.write('\\n' + label('Description') + '\\n');\n\t\t// stripHtml only removes tags; sanitize afterwards in case the rich-text source\n\t\t// embedded raw escape sequences inside text nodes. Use the multiline variant so the\n\t\t// description's paragraph breaks (\\n) are preserved while \\r / \\t / other controls\n\t\t// are still stripped.\n\t\tprocess.stdout.write(renderDescription(job.job_description) + '\\n');\n\t}\n\n\tprocess.stdout.write(\n\t\t'\\n' +\n\t\t\tdim(\n\t\t\t\t'Tip: use --output json (or --field <dotted.path>, e.g. --field job_info.salary_display) for scripting.',\n\t\t\t\tcolor\n\t\t\t) +\n\t\t\t'\\n'\n\t);\n}\n\n/**\n * 後端 job_description 可能含 HTML(rich text);CLI 終端機列印時剝掉 tag。\n * 不做完整 HTML 解析 —— 只把 tag 拿掉、& 實體做最常見的還原。\n */\nfunction stripHtml(s: string): string {\n\treturn s\n\t\t.replace(/<\\/?(p|br|div|li|h[1-6])[^>]*>/gi, '\\n')\n\t\t.replace(/<[^>]+>/g, '')\n\t\t.replace(/ /g, ' ')\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/"/g, '\"')\n\t\t.replace(/'/g, \"'\")\n\t\t.replace(/\\n{3,}/g, '\\n\\n')\n\t\t.trim();\n}\n\nfunction renderDescription(html: string): string {\n\treturn sanitizeForTerminalMultiline(stripHtml(html));\n}\n\n// Internal helpers exposed for tests. Not part of the public CLI API.\nexport const __test__ = {\n\tstripHtml,\n\tgetPath,\n\trenderDescription,\n\trunBatch,\n\tmakeBatchProjector,\n\tparseBatchInput,\n\tresolveBatchConcurrency,\n};\n","import type { Command } from 'commander';\nimport { registerJobsSearch } from './search';\nimport { registerJobsView } from './view';\n\nexport function registerJobsCommand(program: Command): void {\n\tconst jobs = program.command('jobs').description('Search and view public job listings');\n\tregisterJobsSearch(jobs);\n\tregisterJobsView(jobs);\n}\n","import type { Command } from 'commander';\nimport { isConfigKey, isDeprecatedConfigKey, loadConfig, saveConfig, validateAndCoerce } from '../../lib/config-store';\nimport { CliError, ExitCode } from '../../lib/errors';\nimport { API_BASE_ENV_VAR } from '../../lib/global-opts';\n\nexport function registerConfigSet(parent: Command): void {\n\tparent\n\t\t.command('set <key> <value>')\n\t\t.description('Set a config value. Keys: locale, output, timeout_ms')\n\t\t.action((key: string, value: string) => {\n\t\t\tif (isDeprecatedConfigKey(key)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Config key \"${key}\" was removed in 0.1.2. Set the ${API_BASE_ENV_VAR} env var ` +\n\t\t\t\t\t\t`(or use the --api flag) instead.`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!isConfigKey(key)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Unknown config key \"${key}\". Allowed: locale, output, timeout_ms`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst coerced = validateAndCoerce(key, value);\n\t\t\tconst config = loadConfig();\n\t\t\t// Object.assign 形式避免 TS 5.5 在 `config[key] = coerced` 上推不出\n\t\t\t// ConfigValueMap[K]→CliConfig[K] 的對應;validateAndCoerce 已 generic、型別正確。\n\t\t\tObject.assign(config, { [key]: coerced });\n\t\t\tsaveConfig(config);\n\t\t\tprocess.stdout.write(`Set ${key} = ${JSON.stringify(coerced)}\\n`);\n\t\t});\n}\n","import type { Command } from 'commander';\nimport { isConfigKey, loadConfig } from '../../lib/config-store';\nimport { CliError, ExitCode } from '../../lib/errors';\nimport { printJson } from '../../lib/output';\n\nexport function registerConfigGet(parent: Command): void {\n\tparent\n\t\t.command('get [key]')\n\t\t.description('Print config value(s). With no key, prints the whole config as JSON.')\n\t\t.action((key: string | undefined) => {\n\t\t\tconst config = loadConfig();\n\t\t\tif (key === undefined) {\n\t\t\t\tprintJson(config);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!isConfigKey(key)) {\n\t\t\t\tthrow new CliError(\n\t\t\t\t\t`Unknown config key \"${key}\". Allowed: locale, output, timeout_ms`,\n\t\t\t\t\tExitCode.InvalidArgument\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst value = (config as Record<string, unknown>)[key];\n\t\t\tif (value === undefined) {\n\t\t\t\tprocess.stdout.write('\\n');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprocess.stdout.write((typeof value === 'string' ? value : JSON.stringify(value)) + '\\n');\n\t\t});\n}\n","import type { Command } from 'commander';\nimport { getConfigPath } from '../../lib/config-store';\n\nexport function registerConfigPath(parent: Command): void {\n\tparent\n\t\t.command('path')\n\t\t.description('Print the path to the config file (regardless of whether it exists)')\n\t\t.action(() => {\n\t\t\tprocess.stdout.write(getConfigPath() + '\\n');\n\t\t});\n}\n","import type { Command } from 'commander';\nimport { existsSync, unlinkSync } from 'node:fs';\nimport { getConfigPath } from '../../lib/config-store';\nimport { InvalidArgumentError } from '../../lib/errors';\n\nexport function registerConfigReset(parent: Command): void {\n\tparent\n\t\t.command('reset')\n\t\t.description('Delete the config file')\n\t\t.option('-f, --force', 'skip the confirmation prompt')\n\t\t.action(async (opts: { force?: boolean }) => {\n\t\t\tconst path = getConfigPath();\n\t\t\tif (!existsSync(path)) {\n\t\t\t\tprocess.stdout.write('No config file to delete.\\n');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!opts.force) {\n\t\t\t\tconst ok = await promptYesNo(`Delete config at ${path}? [y/N] `);\n\t\t\t\tif (!ok) {\n\t\t\t\t\tprocess.stdout.write('Aborted.\\n');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tunlinkSync(path);\n\t\t\t} catch (err) {\n\t\t\t\tconst e = err as NodeJS.ErrnoException;\n\t\t\t\tif (e.code === 'ENOENT') {\n\t\t\t\t\t// Race with another process: someone deleted it between exists & unlink.\n\t\t\t\t\tprocess.stdout.write('No config file to delete (already removed).\\n');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthrow new InvalidArgumentError(`Failed to delete config at ${path}: ${e.message}`);\n\t\t\t}\n\t\t\tprocess.stdout.write(`Deleted ${path}\\n`);\n\t\t});\n}\n\nfunction promptYesNo(prompt: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tprocess.stdout.write(prompt);\n\t\tlet buf = '';\n\t\tprocess.stdin.setEncoding('utf8');\n\t\tconst onData = (chunk: string) => {\n\t\t\tbuf += chunk;\n\t\t\tconst nl = buf.indexOf('\\n');\n\t\t\tif (nl >= 0) {\n\t\t\t\tcleanup();\n\t\t\t\tconst answer = buf.slice(0, nl).trim().toLowerCase();\n\t\t\t\tresolve(answer === 'y' || answer === 'yes');\n\t\t\t}\n\t\t};\n\t\tconst onEnd = () => {\n\t\t\tcleanup();\n\t\t\tprocess.stdout.write('\\n(no input — aborting)\\n');\n\t\t\tresolve(false);\n\t\t};\n\t\tconst cleanup = () => {\n\t\t\tprocess.stdin.removeListener('data', onData);\n\t\t\tprocess.stdin.removeListener('end', onEnd);\n\t\t\tprocess.stdin.pause();\n\t\t};\n\t\tprocess.stdin.on('data', onData);\n\t\tprocess.stdin.on('end', onEnd);\n\t});\n}\n","import type { Command } from 'commander';\nimport { registerConfigSet } from './set';\nimport { registerConfigGet } from './get';\nimport { registerConfigPath } from './path';\nimport { registerConfigReset } from './reset';\n\nexport function registerConfigCommand(program: Command): void {\n\tconst config = program.command('config').description('Manage CLI configuration');\n\tregisterConfigSet(config);\n\tregisterConfigGet(config);\n\tregisterConfigPath(config);\n\tregisterConfigReset(config);\n}\n","import type { Command } from 'commander';\nimport { existsSync } from 'node:fs';\nimport { createApiClient } from '../lib/api-client';\nimport { resolveContext } from '../lib/global-opts';\nimport { getConfigPath } from '../lib/config-store';\nimport { ExitCode } from '../lib/errors';\n\n/**\n * Query params the server accepts syntactically but silently ignores — surfacing them\n * here is the whole point: an agent reading `wport doctor` learns not to try to control\n * sort order (it can't), instead of discovering it the hard way via wrong-but-no-error\n * results. The CLI deliberately doesn't expose flags for these.\n */\nexport const SILENT_IGNORED_PARAMS = ['orderBy', 'order'];\n\nexport function registerDoctorCommand(program: Command): void {\n\tprogram\n\t\t.command('doctor')\n\t\t.description(\n\t\t\t'Diagnose CLI setup: resolved config, server reachability, schema fingerprint, and known server quirks.'\n\t\t)\n\t\t.action(async (_opts: unknown, command: Command) => {\n\t\t\tconst ctx = resolveContext(command);\n\t\t\tconst line = (s = '') => process.stdout.write(s + '\\n');\n\n\t\t\tline(`wport-cli ${__CLI_VERSION__}`);\n\t\t\tline(` bundled schema fingerprint: ${__SCHEMA_HASH__}`);\n\t\t\tline('');\n\n\t\t\tline('Resolved configuration:');\n\t\t\tline(` API base URL: ${ctx.baseUrl}`);\n\t\t\tline(` locale: ${ctx.locale}`);\n\t\t\tline(` timeout: ${ctx.timeoutMs}ms`);\n\t\t\tconst cfgPath = getConfigPath();\n\t\t\tline(` config file: ${cfgPath}${existsSync(cfgPath) ? '' : ' (not present)'}`);\n\t\t\tline('');\n\n\t\t\tline('Server connectivity:');\n\t\t\tconst reachable = await probeServer(ctx, line);\n\t\t\tline('');\n\n\t\t\tline('Known server behaviours (read this before scripting an agent):');\n\t\t\tline(\n\t\t\t\t` • Sort is server-controlled. These query params are silently ignored: ${SILENT_IGNORED_PARAMS.join(', ')}.`\n\t\t\t);\n\t\t\tline(' • jobs search sorts by publish date, or by relevance when --keyword is set.');\n\t\t\tline(' • jobs view --batch caps parallelism (default 5, max 20) to stay friendly to the API.');\n\t\t\tline('');\n\n\t\t\tline('Schema drift:');\n\t\t\tline(' The fingerprint above identifies the OpenAPI contract this CLI was built against.');\n\t\t\tline(' Automated drift detection needs a server-side schema-version endpoint, which is');\n\t\t\tline(' not available yet — for now, compare fingerprints manually after a server release. [TODO]');\n\n\t\t\tif (!reachable) process.exit(ExitCode.ServerOrNetworkError);\n\t\t});\n}\n\n/**\n * Lightweight reachability probe: a 1-result search hits the real public endpoint\n * without pulling a meaningful payload. A network-layer failure is a hard \"unreachable\"\n * (caller exits non-zero); an HTTP response of any status still proves the host is\n * reachable, so we report the status but don't treat it as a connectivity failure.\n */\nasync function probeServer(\n\tctx: { baseUrl: string; locale: string; timeoutMs: number },\n\tline: (s?: string) => void\n): Promise<boolean> {\n\ttry {\n\t\tconst client = createApiClient({ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs });\n\t\tconst { response } = await client.GET('/api/jobs/search', { params: { query: { pageSize: 1 } } });\n\t\tif (response.ok) {\n\t\t\tline(` ✓ reachable (HTTP ${response.status})`);\n\t\t} else {\n\t\t\tline(` ! reachable, but server responded HTTP ${response.status}`);\n\t\t}\n\t\treturn true;\n\t} catch (err) {\n\t\tline(` ✗ unreachable: ${err instanceof Error ? err.message : String(err)}`);\n\t\treturn false;\n\t}\n}\n"],"mappings":"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","names":["pc","Table","import_node_fs","createClient","envPaths","paths","import_node_fs","import_picocolors","paths","s","pc","program","import_node_fs","program","import_node_fs","program"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wport/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "wport CLI — terminal interface to the W101 Talent Search Hub public API",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/w101-admin/W101-TalentSearchHub.git",
|
|
8
|
+
"directory": "apps/cli"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/w101-admin/W101-TalentSearchHub/tree/main/apps/cli#readme",
|
|
11
|
+
"author": "YAO <yao@wport.me>",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"email": "yao@wport.me"
|
|
14
|
+
},
|
|
5
15
|
"bin": {
|
|
6
16
|
"wport": "bin/wport.js"
|
|
7
17
|
},
|
|
@@ -14,6 +24,7 @@
|
|
|
14
24
|
"bin/",
|
|
15
25
|
"dist/",
|
|
16
26
|
"README.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
17
28
|
"LICENSE"
|
|
18
29
|
],
|
|
19
30
|
"scripts": {
|
|
@@ -33,11 +44,17 @@
|
|
|
33
44
|
},
|
|
34
45
|
"keywords": [
|
|
35
46
|
"wport",
|
|
47
|
+
"wport-cli",
|
|
36
48
|
"w101",
|
|
37
49
|
"jobs",
|
|
38
50
|
"cli",
|
|
39
51
|
"talent",
|
|
40
|
-
"job-search"
|
|
52
|
+
"job-search",
|
|
53
|
+
"job-search-api",
|
|
54
|
+
"ndjson",
|
|
55
|
+
"jq",
|
|
56
|
+
"agent",
|
|
57
|
+
"terminal"
|
|
41
58
|
],
|
|
42
59
|
"license": "MIT",
|
|
43
60
|
"dependencies": {
|
|
@@ -45,7 +62,6 @@
|
|
|
45
62
|
"commander": "^12.1.0",
|
|
46
63
|
"env-paths": "^3.0.0",
|
|
47
64
|
"openapi-fetch": "^0.12.0",
|
|
48
|
-
"ora": "^8.1.0",
|
|
49
65
|
"picocolors": "^1.0.1"
|
|
50
66
|
},
|
|
51
67
|
"devDependencies": {
|