@wport/cli 0.1.1
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/LICENSE +21 -0
- package/README.md +167 -0
- package/bin/wport.js +4 -0
- package/dist/index.js +721 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hotfire Digital Information Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @wport/cli
|
|
2
|
+
|
|
3
|
+
`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.
|
|
4
|
+
|
|
5
|
+
> **Status**: 0.1.x pre-release.
|
|
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.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @wport/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js >= 18.17.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Search jobs
|
|
26
|
+
wport jobs search --keyword backend
|
|
27
|
+
|
|
28
|
+
# View a specific job (enc_id from the search result)
|
|
29
|
+
wport jobs view <enc_id>
|
|
30
|
+
|
|
31
|
+
# Pipe to jq for scripting
|
|
32
|
+
wport jobs search --keyword backend --output json | jq '.data[].title'
|
|
33
|
+
|
|
34
|
+
# Switch language (affects API response, not CLI strings)
|
|
35
|
+
wport jobs search --keyword backend --lang en-US
|
|
36
|
+
|
|
37
|
+
# Persist your preferences (default base URL is https://api.wport.me;
|
|
38
|
+
# override for local dev)
|
|
39
|
+
wport config set locale zh-TW
|
|
40
|
+
wport config set api_base_url http://localhost:3000 # only for local dev
|
|
41
|
+
wport config get
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
### `wport jobs search [options]`
|
|
47
|
+
|
|
48
|
+
| Flag | Description |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `-k, --keyword <text>` | Keyword search (title / company name etc.) |
|
|
51
|
+
| `-l, --location <code>` | Area code (repeatable, e.g. `6001001000`) |
|
|
52
|
+
| `-c, --category <code>` | Job classification code (repeatable) |
|
|
53
|
+
| `-p, --page <n>` | Page number (default 1) |
|
|
54
|
+
| `-s, --page-size <n>` | Page size (default 10, max 100) |
|
|
55
|
+
| `--json-query <file>` | Advanced: read full query body from a JSON file |
|
|
56
|
+
|
|
57
|
+
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
|
+
|
|
59
|
+
**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
|
+
|
|
61
|
+
JSON response shape (paginated, flat):
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"success": true,
|
|
66
|
+
"statusCode": 200,
|
|
67
|
+
"message": "...",
|
|
68
|
+
"data": [ { "enc_id": "...", "title": "...", ... } ],
|
|
69
|
+
"currentPage": 1,
|
|
70
|
+
"totalPages": 5,
|
|
71
|
+
"pageSize": 10,
|
|
72
|
+
"totalCount": 47
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `wport jobs view <enc_id> [options]`
|
|
77
|
+
|
|
78
|
+
| Flag | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `--field <path>` | Output a single field (supports dotted paths, e.g. `--field job_info.salary_display`) |
|
|
81
|
+
|
|
82
|
+
Supports stdin: `echo "<enc_id>" \| wport jobs view -`
|
|
83
|
+
|
|
84
|
+
The response is the nested `JobViewVM`:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
company_header_info.{ company_name, company_icon_url, enc_company_id, ... }
|
|
88
|
+
job_info.{ job_title, area_display, salary_display, experience_display, ... }
|
|
89
|
+
job_description # HTML rich text
|
|
90
|
+
job_information / recruitment_conditions / benefits / about_company
|
|
91
|
+
structured_data # JSON-LD for SEO
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Table mode prints the highlights from `company_header_info` + `job_info` + the stripped `job_description`. Use `--output json` to get the full structure for scripting.
|
|
95
|
+
|
|
96
|
+
### `wport config <set|get|path|reset>`
|
|
97
|
+
|
|
98
|
+
Configurable keys: `locale`, `api_base_url`, `output`, `timeout_ms`.
|
|
99
|
+
|
|
100
|
+
Config file location varies by OS (see `wport config path`). Stored with mode `0600`.
|
|
101
|
+
|
|
102
|
+
## Global flags
|
|
103
|
+
|
|
104
|
+
| Flag | Default |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `--lang <locale>` | config → `zh-TW`. Allowed: `zh-TW`, `en-US`, `vi-VN`, `th-TH`, `id-ID` |
|
|
107
|
+
| `--api <url>` | config → `https://api.wport.me` (production) |
|
|
108
|
+
| `--output <fmt>` | `table` (TTY) / `json` (when piped) |
|
|
109
|
+
| `--no-color` | auto via `NO_COLOR` env |
|
|
110
|
+
| `--timeout <ms>` | 10000 |
|
|
111
|
+
|
|
112
|
+
## Exit codes
|
|
113
|
+
|
|
114
|
+
| Code | Meaning |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `0` | Success |
|
|
117
|
+
| `2` | Invalid arguments |
|
|
118
|
+
| `3` | Server returned 4xx |
|
|
119
|
+
| `4` | Server returned 5xx / network error |
|
|
120
|
+
| `5` | Config file corrupted |
|
|
121
|
+
|
|
122
|
+
## Rate limit notice
|
|
123
|
+
|
|
124
|
+
The public job search endpoint is throttled to 1200 requests / minute / client IP. For human interactive use this is plenty. **Do not** spin up multi-process scrapers — your IP will be blocked.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cd apps/cli
|
|
130
|
+
npm install
|
|
131
|
+
npm run gen:openapi:prod # fetch + filter swagger from prod, produces src/generated/schema.d.ts
|
|
132
|
+
npm run build
|
|
133
|
+
npm link # so the wport command resolves to this build
|
|
134
|
+
|
|
135
|
+
wport jobs search --keyword backend
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Three codegen variants — pick whichever matches your setup:
|
|
139
|
+
|
|
140
|
+
| Script | Source | When to use |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `npm run gen:openapi:prod` | `https://api.wport.me/api/api-doc-json` | Default; works from a fresh checkout, no local backend needed |
|
|
143
|
+
| `npm run gen:openapi:remote` | `http://localhost:3000/api/api-doc-json` | When you're hacking on the backend locally |
|
|
144
|
+
| `npm run gen:openapi` | `../../docs/swagger/swagger-dev.json` | When you've already exported the spec via root's `npm run export:swagger:json` |
|
|
145
|
+
|
|
146
|
+
The `:prod` / `:remote` variants go through `scripts/gen-from-remote.cjs`, which filters the upstream spec down to the endpoints this CLI uses (it ignores unrelated `$ref` breakage elsewhere in the spec). When the CLI starts using a new endpoint, add it to `KEEP_PATHS` in that script.
|
|
147
|
+
|
|
148
|
+
## Support
|
|
149
|
+
|
|
150
|
+
This CLI is in pre-release. Bug reports and feature requests are best-effort and handled directly by the maintainer.
|
|
151
|
+
|
|
152
|
+
**During the `0.x.y` phase**, please contact:
|
|
153
|
+
|
|
154
|
+
- **Email**: [yao@wport.me](mailto:yao@wport.me)
|
|
155
|
+
- **Response time**: best-effort, typically within a few business days
|
|
156
|
+
|
|
157
|
+
A public issue tracker (GitHub Issues under [`w101-admin/W101-TalentSearchHub`](https://github.com/w101-admin/W101-TalentSearchHub/issues)) will be opened once usage volume justifies it. Until then, email is the only supported channel — please don't open issues on the main repo for CLI-specific concerns yet (the repo is shared with the backend and would create cross-team noise).
|
|
158
|
+
|
|
159
|
+
## Compatibility
|
|
160
|
+
|
|
161
|
+
This CLI tracks the `api.wport.me` public job endpoints (`GET /api/jobs/search`, `GET /api/jobs/{encId}/view`).
|
|
162
|
+
|
|
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)).
|
|
164
|
+
|
|
165
|
+
**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
|
+
|
|
167
|
+
Until `1.0.0` ships, treat this CLI as an early-access tool. Open an issue or contact the maintainer before building production automation on top of it.
|
package/bin/wport.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/lib/errors.ts
|
|
30
|
+
var ExitCode = {
|
|
31
|
+
Success: 0,
|
|
32
|
+
InvalidArgument: 2,
|
|
33
|
+
ServerClientError: 3,
|
|
34
|
+
ServerOrNetworkError: 4,
|
|
35
|
+
ConfigCorrupt: 5
|
|
36
|
+
};
|
|
37
|
+
var CliError = class extends Error {
|
|
38
|
+
exitCode;
|
|
39
|
+
constructor(message, exitCode) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "CliError";
|
|
42
|
+
this.exitCode = exitCode;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var InvalidArgumentError = class extends CliError {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message, ExitCode.InvalidArgument);
|
|
48
|
+
this.name = "InvalidArgumentError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var ServerClientHttpError = class extends CliError {
|
|
52
|
+
status;
|
|
53
|
+
body;
|
|
54
|
+
constructor(message, status, body) {
|
|
55
|
+
super(message, ExitCode.ServerClientError);
|
|
56
|
+
this.name = "ServerClientHttpError";
|
|
57
|
+
this.status = status;
|
|
58
|
+
this.body = body;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var NetworkError = class extends CliError {
|
|
62
|
+
cause;
|
|
63
|
+
constructor(message, cause) {
|
|
64
|
+
super(message, ExitCode.ServerOrNetworkError);
|
|
65
|
+
this.name = "NetworkError";
|
|
66
|
+
this.cause = cause;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
function isCliError(err) {
|
|
70
|
+
return err instanceof CliError;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/lib/output.ts
|
|
74
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
75
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
76
|
+
var ANSI_ESCAPE_SEQUENCE = new RegExp(
|
|
77
|
+
[
|
|
78
|
+
// CSI: ESC [ params intermediates final
|
|
79
|
+
"\\u001B\\[[0-?]*[ -/]*[@-~]",
|
|
80
|
+
// OSC: ESC ] payload (any chars except BEL/ESC) terminated by BEL or ESC \
|
|
81
|
+
"\\u001B\\][^\\u0007\\u001B]*(?:\\u0007|\\u001B\\\\)",
|
|
82
|
+
// Two-char escapes: ESC + Fe final byte (0x40-0x5F = @ A B ... Z [ \ ] ^ _).
|
|
83
|
+
// 涵蓋 CSI([) / OSC(]) / DCS(P) / SOS(X) / ST(\\) / PM(^) / APC(_) intro。
|
|
84
|
+
// CSI / OSC regex 在前面 OR-分支會先匹配對應序列;這條兜底所有未覆蓋 Fe。
|
|
85
|
+
"\\u001B[@-_]"
|
|
86
|
+
].join("|"),
|
|
87
|
+
"g"
|
|
88
|
+
);
|
|
89
|
+
var CONTROL_CHARS_STRICT = new RegExp("[\\u0000-\\u001F\\u007F\\u0080-\\u009F]", "g");
|
|
90
|
+
var CONTROL_CHARS_MULTILINE = new RegExp("[\\u0000-\\u0009\\u000B-\\u001F\\u007F\\u0080-\\u009F]", "g");
|
|
91
|
+
function sanitizeForTerminal(s) {
|
|
92
|
+
return s.replace(ANSI_ESCAPE_SEQUENCE, "").replace(CONTROL_CHARS_STRICT, "");
|
|
93
|
+
}
|
|
94
|
+
function sanitizeForTerminalMultiline(s) {
|
|
95
|
+
return s.replace(ANSI_ESCAPE_SEQUENCE, "").replace(/\r\n?/g, "\n").replace(CONTROL_CHARS_MULTILINE, "");
|
|
96
|
+
}
|
|
97
|
+
function resolveOutputFormat(explicit) {
|
|
98
|
+
if (explicit === void 0) {
|
|
99
|
+
return process.stdout.isTTY ? "table" : "json";
|
|
100
|
+
}
|
|
101
|
+
if (explicit !== "json" && explicit !== "table") {
|
|
102
|
+
throw new CliError(`Invalid --output "${explicit}". Allowed: table, json`, ExitCode.InvalidArgument);
|
|
103
|
+
}
|
|
104
|
+
return explicit;
|
|
105
|
+
}
|
|
106
|
+
function isColorEnabled(noColor) {
|
|
107
|
+
if (noColor === true) return false;
|
|
108
|
+
if (process.env.NO_COLOR) return false;
|
|
109
|
+
return process.stdout.isTTY ?? false;
|
|
110
|
+
}
|
|
111
|
+
function printJson(value) {
|
|
112
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
113
|
+
}
|
|
114
|
+
function printTable(rows, columns, color) {
|
|
115
|
+
if (rows.length === 0) {
|
|
116
|
+
process.stdout.write(color ? import_picocolors.default.dim("(no results)\n") : "(no results)\n");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const table = new import_cli_table3.default({
|
|
120
|
+
head: columns.map((c) => color ? import_picocolors.default.bold(c.header) : c.header),
|
|
121
|
+
style: { head: [], border: [] },
|
|
122
|
+
colWidths: columns.map((c) => c.maxWidth ?? null),
|
|
123
|
+
wordWrap: true
|
|
124
|
+
});
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
table.push(columns.map((c) => sanitizeForTerminal(c.value(row))));
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write(table.toString() + "\n");
|
|
129
|
+
}
|
|
130
|
+
function printError(message, color) {
|
|
131
|
+
const prefix = color ? import_picocolors.default.red("Error:") : "Error:";
|
|
132
|
+
process.stderr.write(`${prefix} ${sanitizeForTerminal(message)}
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
function printWarn(message, color) {
|
|
136
|
+
const prefix = color ? import_picocolors.default.yellow("Warning:") : "Warning:";
|
|
137
|
+
process.stderr.write(`${prefix} ${sanitizeForTerminal(message)}
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
function dim(text, color) {
|
|
141
|
+
return color ? import_picocolors.default.dim(text) : text;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/commands/jobs/search.ts
|
|
145
|
+
var import_node_fs2 = require("fs");
|
|
146
|
+
|
|
147
|
+
// src/lib/api-client.ts
|
|
148
|
+
var import_openapi_fetch = __toESM(require("openapi-fetch"));
|
|
149
|
+
function createApiClient(opts) {
|
|
150
|
+
return (0, import_openapi_fetch.default)({
|
|
151
|
+
baseUrl: opts.baseUrl,
|
|
152
|
+
headers: {
|
|
153
|
+
"Accept-Language": opts.locale,
|
|
154
|
+
"User-Agent": buildUserAgent(),
|
|
155
|
+
Accept: "application/json"
|
|
156
|
+
},
|
|
157
|
+
fetch: (request) => fetchWithTimeout(request, opts.timeoutMs)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function fetchWithTimeout(request, timeoutMs) {
|
|
161
|
+
const timedRequest = new Request(request, { signal: AbortSignal.timeout(timeoutMs) });
|
|
162
|
+
return fetch(timedRequest).catch((err) => {
|
|
163
|
+
if (isTimeoutAbort(err)) {
|
|
164
|
+
throw new NetworkError(`Request timed out after ${timeoutMs}ms`, err);
|
|
165
|
+
}
|
|
166
|
+
const code = err?.cause?.code;
|
|
167
|
+
const detail = code ? code : err?.message ?? String(err);
|
|
168
|
+
throw new NetworkError(`Cannot reach upstream: ${detail}`, err);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function isTimeoutAbort(err) {
|
|
172
|
+
if (err && typeof err === "object" && "name" in err) {
|
|
173
|
+
const name = err.name;
|
|
174
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
function buildUserAgent() {
|
|
179
|
+
return `wport-cli/${"0.1.1"} (node ${process.version}; ${process.platform})`;
|
|
180
|
+
}
|
|
181
|
+
function unwrapDataResponse(body) {
|
|
182
|
+
if (body && typeof body === "object" && "success" in body && "data" in body) {
|
|
183
|
+
return body.data;
|
|
184
|
+
}
|
|
185
|
+
throw new CliError("Unexpected response shape: missing { success, data } wrapper", ExitCode.ServerOrNetworkError);
|
|
186
|
+
}
|
|
187
|
+
function asPaginatedBody(body) {
|
|
188
|
+
if (!body || typeof body !== "object") {
|
|
189
|
+
throw new CliError("Unexpected response shape: not an object", ExitCode.ServerOrNetworkError);
|
|
190
|
+
}
|
|
191
|
+
const b = body;
|
|
192
|
+
if (!Array.isArray(b.data)) {
|
|
193
|
+
throw new CliError("Unexpected response shape: missing `data` array", ExitCode.ServerOrNetworkError);
|
|
194
|
+
}
|
|
195
|
+
for (const key of ["currentPage", "totalPages", "pageSize", "totalCount"]) {
|
|
196
|
+
if (typeof b[key] !== "number") {
|
|
197
|
+
throw new CliError(`Unexpected response shape: missing or non-numeric "${key}"`, ExitCode.ServerOrNetworkError);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return body;
|
|
201
|
+
}
|
|
202
|
+
function throwForHttpStatus(status, body) {
|
|
203
|
+
const message = extractErrorMessage(body) ?? `HTTP ${status}`;
|
|
204
|
+
if (status >= 400 && status < 500) {
|
|
205
|
+
throw new ServerClientHttpError(message, status, body);
|
|
206
|
+
}
|
|
207
|
+
throw new CliError(message, ExitCode.ServerOrNetworkError);
|
|
208
|
+
}
|
|
209
|
+
function extractErrorMessage(body) {
|
|
210
|
+
if (typeof body === "string") return body;
|
|
211
|
+
if (body && typeof body === "object") {
|
|
212
|
+
const obj = body;
|
|
213
|
+
if (Array.isArray(obj.message)) {
|
|
214
|
+
const parts = obj.message.filter((m) => typeof m === "string");
|
|
215
|
+
if (parts.length > 0) return parts.join("; ");
|
|
216
|
+
}
|
|
217
|
+
if (typeof obj.message === "string") return obj.message;
|
|
218
|
+
if (obj.message && typeof obj.message === "object") {
|
|
219
|
+
try {
|
|
220
|
+
return JSON.stringify(obj.message);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (typeof obj.error === "string") return obj.error;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/lib/config-store.ts
|
|
230
|
+
var import_node_fs = require("fs");
|
|
231
|
+
var import_node_path = require("path");
|
|
232
|
+
var import_env_paths = __toESM(require("env-paths"));
|
|
233
|
+
var ALLOWED_LOCALES = ["zh-TW", "en-US", "vi-VN", "th-TH", "id-ID"];
|
|
234
|
+
var ALLOWED_OUTPUT = ["table", "json"];
|
|
235
|
+
var CONFIG_KEYS = ["locale", "api_base_url", "output", "timeout_ms"];
|
|
236
|
+
function isConfigKey(key) {
|
|
237
|
+
return CONFIG_KEYS.includes(key);
|
|
238
|
+
}
|
|
239
|
+
var paths = (0, import_env_paths.default)("wport", { suffix: "" });
|
|
240
|
+
function getConfigPath() {
|
|
241
|
+
return (0, import_node_path.join)(paths.config, "config.json");
|
|
242
|
+
}
|
|
243
|
+
function parseConfig(raw) {
|
|
244
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
245
|
+
throw new CliError("Config must be a JSON object", ExitCode.ConfigCorrupt);
|
|
246
|
+
}
|
|
247
|
+
const input = raw;
|
|
248
|
+
const out = {};
|
|
249
|
+
for (const key of CONFIG_KEYS) {
|
|
250
|
+
if (!(key in input)) continue;
|
|
251
|
+
const value = input[key];
|
|
252
|
+
try {
|
|
253
|
+
const coerced = validateAndCoerce(key, String(value));
|
|
254
|
+
Object.assign(out, { [key]: coerced });
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err instanceof CliError) {
|
|
257
|
+
throw new CliError(`Config key "${key}" invalid: ${err.message}`, ExitCode.ConfigCorrupt);
|
|
258
|
+
}
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function loadConfig() {
|
|
265
|
+
const path = getConfigPath();
|
|
266
|
+
if (!(0, import_node_fs.existsSync)(path)) return {};
|
|
267
|
+
let raw;
|
|
268
|
+
try {
|
|
269
|
+
raw = (0, import_node_fs.readFileSync)(path, "utf8");
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw new CliError(`Failed to read config at ${path}: ${err.message}`, ExitCode.ConfigCorrupt);
|
|
272
|
+
}
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
parsed = JSON.parse(raw);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
throw new CliError(`Failed to parse JSON at ${path}: ${err.message}`, ExitCode.ConfigCorrupt);
|
|
278
|
+
}
|
|
279
|
+
return parseConfig(parsed);
|
|
280
|
+
}
|
|
281
|
+
function saveConfig(config) {
|
|
282
|
+
const path = getConfigPath();
|
|
283
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
284
|
+
const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
285
|
+
const fd = (0, import_node_fs.openSync)(tmpPath, "w", 384);
|
|
286
|
+
try {
|
|
287
|
+
(0, import_node_fs.writeSync)(fd, JSON.stringify(config, null, 2) + "\n");
|
|
288
|
+
} catch (err) {
|
|
289
|
+
(0, import_node_fs.closeSync)(fd);
|
|
290
|
+
try {
|
|
291
|
+
(0, import_node_fs.unlinkSync)(tmpPath);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
(0, import_node_fs.closeSync)(fd);
|
|
297
|
+
if (process.platform !== "win32") {
|
|
298
|
+
try {
|
|
299
|
+
(0, import_node_fs.chmodSync)(tmpPath, 384);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
printWarn(
|
|
302
|
+
`Failed to chmod 0600 on config tmpfile: ${err.message}. Other users on this system may be able to read CLI config.`,
|
|
303
|
+
false
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
(0, import_node_fs.renameSync)(tmpPath, path);
|
|
308
|
+
}
|
|
309
|
+
function validateAndCoerce(key, value) {
|
|
310
|
+
switch (key) {
|
|
311
|
+
case "locale": {
|
|
312
|
+
if (!ALLOWED_LOCALES.includes(value)) {
|
|
313
|
+
throw new CliError(
|
|
314
|
+
`Invalid locale "${value}". Allowed: ${ALLOWED_LOCALES.join(", ")}`,
|
|
315
|
+
ExitCode.InvalidArgument
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
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
|
+
case "output": {
|
|
333
|
+
if (!ALLOWED_OUTPUT.includes(value)) {
|
|
334
|
+
throw new CliError(
|
|
335
|
+
`Invalid output "${value}". Allowed: ${ALLOWED_OUTPUT.join(", ")}`,
|
|
336
|
+
ExitCode.InvalidArgument
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
case "timeout_ms": {
|
|
342
|
+
const n = Number(value);
|
|
343
|
+
if (!Number.isInteger(n) || n < 100 || n > 6e5) {
|
|
344
|
+
throw new CliError(
|
|
345
|
+
`timeout_ms must be an integer between 100 and 600000 (got ${value})`,
|
|
346
|
+
ExitCode.InvalidArgument
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return n;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/lib/global-opts.ts
|
|
355
|
+
var DEFAULT_BASE_URL = "https://api.wport.me";
|
|
356
|
+
var DEFAULT_LOCALE = "zh-TW";
|
|
357
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
358
|
+
function resolveContext(command) {
|
|
359
|
+
const globals = command.optsWithGlobals();
|
|
360
|
+
const config = loadConfig();
|
|
361
|
+
return {
|
|
362
|
+
baseUrl: resolveBaseUrl(globals.api, config),
|
|
363
|
+
locale: resolveLocale(globals.lang, config),
|
|
364
|
+
timeoutMs: resolveTimeout(globals.timeout, config),
|
|
365
|
+
format: resolveOutputFormat(globals.output),
|
|
366
|
+
color: isColorEnabled(globals.color === false),
|
|
367
|
+
config
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function resolveBaseUrl(override, config) {
|
|
371
|
+
const raw = override ?? config.api_base_url ?? DEFAULT_BASE_URL;
|
|
372
|
+
try {
|
|
373
|
+
new URL(raw);
|
|
374
|
+
} catch {
|
|
375
|
+
throw new CliError(`Invalid api base URL: ${raw}`, ExitCode.InvalidArgument);
|
|
376
|
+
}
|
|
377
|
+
return raw.replace(/\/$/, "");
|
|
378
|
+
}
|
|
379
|
+
function resolveLocale(override, config) {
|
|
380
|
+
const raw = override ?? config.locale ?? DEFAULT_LOCALE;
|
|
381
|
+
if (!ALLOWED_LOCALES.includes(raw)) {
|
|
382
|
+
throw new CliError(`Invalid --lang "${raw}". Allowed: ${ALLOWED_LOCALES.join(", ")}`, ExitCode.InvalidArgument);
|
|
383
|
+
}
|
|
384
|
+
return raw;
|
|
385
|
+
}
|
|
386
|
+
function resolveTimeout(override, config) {
|
|
387
|
+
const raw = override ?? config.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
388
|
+
if (!Number.isInteger(raw) || raw < 100 || raw > 6e5) {
|
|
389
|
+
throw new CliError(`Invalid --timeout ${raw} (must be integer 100..600000)`, ExitCode.InvalidArgument);
|
|
390
|
+
}
|
|
391
|
+
return raw;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/commands/jobs/search.ts
|
|
395
|
+
function registerJobsSearch(parent) {
|
|
396
|
+
parent.command("search").description(
|
|
397
|
+
"Search public job listings. Server sorts by publish date (or relevance when --keyword is set); sort order is not user-configurable."
|
|
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)").action(async (flags, command) => {
|
|
399
|
+
const ctx = resolveContext(command);
|
|
400
|
+
const query = buildQuery(flags);
|
|
401
|
+
const client = createApiClient({
|
|
402
|
+
baseUrl: ctx.baseUrl,
|
|
403
|
+
locale: ctx.locale,
|
|
404
|
+
timeoutMs: ctx.timeoutMs
|
|
405
|
+
});
|
|
406
|
+
const { data, error, response } = await client.GET("/api/jobs/search", {
|
|
407
|
+
params: { query }
|
|
408
|
+
});
|
|
409
|
+
if (!response.ok) throwForHttpStatus(response.status, error);
|
|
410
|
+
const paged = asPaginatedBody(data);
|
|
411
|
+
if (ctx.format === "json") {
|
|
412
|
+
printJson(paged);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
printTable(
|
|
416
|
+
paged.data,
|
|
417
|
+
[
|
|
418
|
+
{ header: "ENC_ID", value: (r) => truncate(r.enc_id ?? "", 14) },
|
|
419
|
+
{ header: "TITLE", value: (r) => r.title ?? "", maxWidth: 36 },
|
|
420
|
+
{ header: "COMPANY", value: (r) => r.company_name ?? "", maxWidth: 20 },
|
|
421
|
+
{ header: "LOCATION", value: (r) => r.area_display ?? "", maxWidth: 18 },
|
|
422
|
+
{ header: "SALARY", value: (r) => r.salary_display ?? "", maxWidth: 18 },
|
|
423
|
+
{ header: "UPDATED", value: (r) => formatDate(r.updated_at), maxWidth: 12 }
|
|
424
|
+
],
|
|
425
|
+
ctx.color
|
|
426
|
+
);
|
|
427
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} results).`;
|
|
428
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport jobs search --page ${paged.currentPage + 1}` : "";
|
|
429
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function buildQuery(flags) {
|
|
433
|
+
if (flags.jsonQuery) {
|
|
434
|
+
return readJsonQuery(flags.jsonQuery);
|
|
435
|
+
}
|
|
436
|
+
const q = {};
|
|
437
|
+
if (flags.keyword) q.keyword = flags.keyword;
|
|
438
|
+
if (flags.location?.length) q.area_codes = flags.location;
|
|
439
|
+
if (flags.category?.length) q.job_classification_codes = flags.category;
|
|
440
|
+
if (flags.page !== void 0) q.currentPage = flags.page;
|
|
441
|
+
if (flags.pageSize !== void 0) q.pageSize = flags.pageSize;
|
|
442
|
+
return q;
|
|
443
|
+
}
|
|
444
|
+
function readJsonQuery(path) {
|
|
445
|
+
let raw;
|
|
446
|
+
try {
|
|
447
|
+
raw = (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
448
|
+
} catch (err) {
|
|
449
|
+
throw new CliError(`Cannot read --json-query file ${path}: ${err.message}`, ExitCode.InvalidArgument);
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const parsed = JSON.parse(raw);
|
|
453
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
Array.isArray(parsed) ? "JSON root is an array; expected an object with query fields" : "JSON root must be an object"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return parsed;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
throw new CliError(`Invalid JSON in ${path}: ${err.message}`, ExitCode.InvalidArgument);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function truncate(s, max) {
|
|
464
|
+
if (s.length <= max) return s;
|
|
465
|
+
const chars = Array.from(s);
|
|
466
|
+
if (chars.length <= max) return s;
|
|
467
|
+
return chars.slice(0, max - 1).join("") + "\u2026";
|
|
468
|
+
}
|
|
469
|
+
function formatDate(s) {
|
|
470
|
+
if (!s) return "";
|
|
471
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(s);
|
|
472
|
+
return m ? m[1] : s;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/commands/jobs/view.ts
|
|
476
|
+
var import_node_fs3 = require("fs");
|
|
477
|
+
|
|
478
|
+
// src/lib/io-helpers.ts
|
|
479
|
+
function ensureStdinPiped(label) {
|
|
480
|
+
if (process.stdin.isTTY) {
|
|
481
|
+
throw new InvalidArgumentError(`${label}: no data on stdin (run via pipe, or pass the value as an arg)`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/commands/jobs/view.ts
|
|
486
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
487
|
+
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 (supports dotted paths, e.g. job_info.job_title)").action(async (encIdArg, flags, command) => {
|
|
489
|
+
const ctx = resolveContext(command);
|
|
490
|
+
const encId = encIdArg === "-" ? readStdin().trim() : encIdArg;
|
|
491
|
+
if (!encId) {
|
|
492
|
+
throw new CliError("enc_id is required", ExitCode.InvalidArgument);
|
|
493
|
+
}
|
|
494
|
+
const client = createApiClient({
|
|
495
|
+
baseUrl: ctx.baseUrl,
|
|
496
|
+
locale: ctx.locale,
|
|
497
|
+
timeoutMs: ctx.timeoutMs
|
|
498
|
+
});
|
|
499
|
+
const { data, error, response } = await client.GET("/api/jobs/{encId}/view", {
|
|
500
|
+
params: { path: { encId } }
|
|
501
|
+
});
|
|
502
|
+
if (!response.ok) throwForHttpStatus(response.status, error);
|
|
503
|
+
const job = unwrapDataResponse(data);
|
|
504
|
+
if (flags.field) {
|
|
505
|
+
const v = getPath(job, flags.field);
|
|
506
|
+
if (v === void 0) {
|
|
507
|
+
throw new CliError(`Field "${flags.field}" not present in response`, ExitCode.InvalidArgument);
|
|
508
|
+
}
|
|
509
|
+
const out = typeof v === "string" ? sanitizeForTerminalMultiline(v) : JSON.stringify(v, null, 2);
|
|
510
|
+
process.stdout.write(out + "\n");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (ctx.format === "json") {
|
|
514
|
+
printJson(job);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
renderJobTable(job, encId, ctx.color);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function renderJobTable(job, encId, color) {
|
|
521
|
+
const label = (s2) => color ? import_picocolors2.default.bold(s2) : s2;
|
|
522
|
+
const s = (v) => v ? sanitizeForTerminal(v) : "";
|
|
523
|
+
const info = job.job_info ?? {};
|
|
524
|
+
const company = job.company_header_info ?? {};
|
|
525
|
+
const lines = [];
|
|
526
|
+
if (info.job_title) lines.push(`${label("Title:")} ${s(info.job_title)}`);
|
|
527
|
+
if (company.company_name) lines.push(`${label("Company:")} ${s(company.company_name)}`);
|
|
528
|
+
if (info.area_display) lines.push(`${label("Location:")} ${s(info.area_display)}`);
|
|
529
|
+
if (info.salary_display) lines.push(`${label("Salary:")} ${s(info.salary_display)}`);
|
|
530
|
+
if (info.job_feature_display) lines.push(`${label("Type:")} ${s(info.job_feature_display)}`);
|
|
531
|
+
if (info.experience_display) lines.push(`${label("Experience:")} ${s(info.experience_display)}`);
|
|
532
|
+
lines.push(dim(`enc_id: ${s(encId)}`, color));
|
|
533
|
+
if (company.enc_company_id) lines.push(dim(`enc_company_id: ${s(company.enc_company_id)}`, color));
|
|
534
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
535
|
+
if (job.job_description) {
|
|
536
|
+
process.stdout.write("\n" + label("Description") + "\n");
|
|
537
|
+
process.stdout.write(renderDescription(job.job_description) + "\n");
|
|
538
|
+
}
|
|
539
|
+
process.stdout.write(
|
|
540
|
+
"\n" + dim(
|
|
541
|
+
"Tip: use --output json (or --field <dotted.path>, e.g. --field job_info.salary_display) for scripting.",
|
|
542
|
+
color
|
|
543
|
+
) + "\n"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
function stripHtml(s) {
|
|
547
|
+
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
|
+
}
|
|
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
|
+
function renderDescription(html) {
|
|
562
|
+
return sanitizeForTerminalMultiline(stripHtml(html));
|
|
563
|
+
}
|
|
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
|
+
|
|
573
|
+
// src/commands/jobs/index.ts
|
|
574
|
+
function registerJobsCommand(program2) {
|
|
575
|
+
const jobs = program2.command("jobs").description("Search and view public job listings");
|
|
576
|
+
registerJobsSearch(jobs);
|
|
577
|
+
registerJobsView(jobs);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/commands/config/set.ts
|
|
581
|
+
function registerConfigSet(parent) {
|
|
582
|
+
parent.command("set <key> <value>").description("Set a config value. Keys: locale, api_base_url, output, timeout_ms").action((key, value) => {
|
|
583
|
+
if (!isConfigKey(key)) {
|
|
584
|
+
throw new CliError(
|
|
585
|
+
`Unknown config key "${key}". Allowed: locale, api_base_url, output, timeout_ms`,
|
|
586
|
+
ExitCode.InvalidArgument
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const coerced = validateAndCoerce(key, value);
|
|
590
|
+
const config = loadConfig();
|
|
591
|
+
Object.assign(config, { [key]: coerced });
|
|
592
|
+
saveConfig(config);
|
|
593
|
+
process.stdout.write(`Set ${key} = ${JSON.stringify(coerced)}
|
|
594
|
+
`);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/commands/config/get.ts
|
|
599
|
+
function registerConfigGet(parent) {
|
|
600
|
+
parent.command("get [key]").description("Print config value(s). With no key, prints the whole config as JSON.").action((key) => {
|
|
601
|
+
const config = loadConfig();
|
|
602
|
+
if (key === void 0) {
|
|
603
|
+
printJson(config);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (!isConfigKey(key)) {
|
|
607
|
+
throw new CliError(
|
|
608
|
+
`Unknown config key "${key}". Allowed: locale, api_base_url, output, timeout_ms`,
|
|
609
|
+
ExitCode.InvalidArgument
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const value = config[key];
|
|
613
|
+
if (value === void 0) {
|
|
614
|
+
process.stdout.write("\n");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
process.stdout.write((typeof value === "string" ? value : JSON.stringify(value)) + "\n");
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/commands/config/path.ts
|
|
622
|
+
function registerConfigPath(parent) {
|
|
623
|
+
parent.command("path").description("Print the path to the config file (regardless of whether it exists)").action(() => {
|
|
624
|
+
process.stdout.write(getConfigPath() + "\n");
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/commands/config/reset.ts
|
|
629
|
+
var import_node_fs4 = require("fs");
|
|
630
|
+
function registerConfigReset(parent) {
|
|
631
|
+
parent.command("reset").description("Delete the config file").option("-f, --force", "skip the confirmation prompt").action(async (opts) => {
|
|
632
|
+
const path = getConfigPath();
|
|
633
|
+
if (!(0, import_node_fs4.existsSync)(path)) {
|
|
634
|
+
process.stdout.write("No config file to delete.\n");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (!opts.force) {
|
|
638
|
+
const ok = await promptYesNo(`Delete config at ${path}? [y/N] `);
|
|
639
|
+
if (!ok) {
|
|
640
|
+
process.stdout.write("Aborted.\n");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
(0, import_node_fs4.unlinkSync)(path);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
const e = err;
|
|
648
|
+
if (e.code === "ENOENT") {
|
|
649
|
+
process.stdout.write("No config file to delete (already removed).\n");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
throw new InvalidArgumentError(`Failed to delete config at ${path}: ${e.message}`);
|
|
653
|
+
}
|
|
654
|
+
process.stdout.write(`Deleted ${path}
|
|
655
|
+
`);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
function promptYesNo(prompt) {
|
|
659
|
+
return new Promise((resolve) => {
|
|
660
|
+
process.stdout.write(prompt);
|
|
661
|
+
let buf = "";
|
|
662
|
+
process.stdin.setEncoding("utf8");
|
|
663
|
+
const onData = (chunk) => {
|
|
664
|
+
buf += chunk;
|
|
665
|
+
const nl = buf.indexOf("\n");
|
|
666
|
+
if (nl >= 0) {
|
|
667
|
+
cleanup();
|
|
668
|
+
const answer = buf.slice(0, nl).trim().toLowerCase();
|
|
669
|
+
resolve(answer === "y" || answer === "yes");
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
const onEnd = () => {
|
|
673
|
+
cleanup();
|
|
674
|
+
process.stdout.write("\n(no input \u2014 aborting)\n");
|
|
675
|
+
resolve(false);
|
|
676
|
+
};
|
|
677
|
+
const cleanup = () => {
|
|
678
|
+
process.stdin.removeListener("data", onData);
|
|
679
|
+
process.stdin.removeListener("end", onEnd);
|
|
680
|
+
process.stdin.pause();
|
|
681
|
+
};
|
|
682
|
+
process.stdin.on("data", onData);
|
|
683
|
+
process.stdin.on("end", onEnd);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/commands/config/index.ts
|
|
688
|
+
function registerConfigCommand(program2) {
|
|
689
|
+
const config = program2.command("config").description("Manage CLI configuration");
|
|
690
|
+
registerConfigSet(config);
|
|
691
|
+
registerConfigGet(config);
|
|
692
|
+
registerConfigPath(config);
|
|
693
|
+
registerConfigReset(config);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/index.ts
|
|
697
|
+
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.1", "-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
|
+
registerJobsCommand(program);
|
|
700
|
+
registerConfigCommand(program);
|
|
701
|
+
program.exitOverride();
|
|
702
|
+
program.parseAsync(process.argv).then(() => process.exit(ExitCode.Success)).catch((err) => handleTopLevelError(err));
|
|
703
|
+
function handleTopLevelError(err) {
|
|
704
|
+
const color = isColorEnabled(false);
|
|
705
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
706
|
+
const commanderErr = err;
|
|
707
|
+
if (commanderErr.code === "commander.helpDisplayed" || commanderErr.code === "commander.version") {
|
|
708
|
+
process.exit(ExitCode.Success);
|
|
709
|
+
}
|
|
710
|
+
if (commanderErr.message) printError(commanderErr.message, color);
|
|
711
|
+
process.exit(ExitCode.InvalidArgument);
|
|
712
|
+
}
|
|
713
|
+
if (isCliError(err)) {
|
|
714
|
+
printError(err.message, color);
|
|
715
|
+
process.exit(err.exitCode);
|
|
716
|
+
}
|
|
717
|
+
const fallbackMessage = err instanceof Error ? err.message : String(err);
|
|
718
|
+
printError(fallbackMessage, color);
|
|
719
|
+
process.exit(ExitCode.ServerOrNetworkError);
|
|
720
|
+
}
|
|
721
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uBAAwB;;;ACAjB,IAAM,WAAW;AAAA,EACvB,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,sBAAsB;AAAA,EACtB,eAAe;AAChB;AAIO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAC1B;AAAA,EAET,YAAY,SAAiB,UAAyB;AACrD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EACjB;AACD;AAEO,IAAM,uBAAN,cAAmC,SAAS;AAAA,EAClD,YAAY,SAAiB;AAC5B,UAAM,SAAS,SAAS,eAAe;AACvC,SAAK,OAAO;AAAA,EACb;AACD;AAEO,IAAM,wBAAN,cAAoC,SAAS;AAAA,EAC1C;AAAA,EACA;AAAA,EACT,YAAY,SAAiB,QAAgB,MAAe;AAC3D,UAAM,SAAS,SAAS,iBAAiB;AACzC,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACb;AACD;AAEO,IAAM,eAAN,cAA2B,SAAS;AAAA,EACjC;AAAA,EACT,YAAY,SAAiB,OAAiB;AAC7C,UAAM,SAAS,SAAS,oBAAoB;AAC5C,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACd;AACD;AAWO,SAAS,WAAW,KAA+B;AACzD,SAAO,eAAe;AACvB;;;AC1DA,wBAAkB;AAClB,wBAAe;AAiBf,IAAM,uBAAuB,IAAI;AAAA,EAChC;AAAA;AAAA,IAEC;AAAA;AAAA,IAEA;AAAA;AAAA;AAAA;AAAA,IAIA;AAAA,EACD,EAAE,KAAK,GAAG;AAAA,EACV;AACD;AAMA,IAAM,uBAAuB,IAAI,OAAO,2CAA2C,GAAG;AAItF,IAAM,0BAA0B,IAAI,OAAO,0DAA0D,GAAG;AAEjG,SAAS,oBAAoB,GAAmB;AACtD,SAAO,EAAE,QAAQ,sBAAsB,EAAE,EAAE,QAAQ,sBAAsB,EAAE;AAC5E;AAEO,SAAS,6BAA6B,GAAmB;AAC/D,SAAO,EAAE,QAAQ,sBAAsB,EAAE,EAAE,QAAQ,UAAU,IAAI,EAAE,QAAQ,yBAAyB,EAAE;AACvG;AAEO,SAAS,oBAAoB,UAA4C;AAC/E,MAAI,aAAa,QAAW;AAC3B,WAAO,QAAQ,OAAO,QAAQ,UAAU;AAAA,EACzC;AACA,MAAI,aAAa,UAAU,aAAa,SAAS;AAChD,UAAM,IAAI,SAAS,qBAAqB,QAAQ,2BAA2B,SAAS,eAAe;AAAA,EACpG;AACA,SAAO;AACR;AAEO,SAAS,eAAe,SAAuC;AACrE,MAAI,YAAY,KAAM,QAAO;AAC7B,MAAI,QAAQ,IAAI,SAAU,QAAO;AACjC,SAAO,QAAQ,OAAO,SAAS;AAChC;AAEO,SAAS,UAAU,OAAsB;AAC/C,UAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,IAAI;AAC3D;AAQO,SAAS,WAAc,MAAW,SAA2B,OAAsB;AACzF,MAAI,KAAK,WAAW,GAAG;AACtB,YAAQ,OAAO,MAAM,QAAQ,kBAAAA,QAAG,IAAI,gBAAgB,IAAI,gBAAgB;AACxE;AAAA,EACD;AACA,QAAM,QAAQ,IAAI,kBAAAC,QAAM;AAAA,IACvB,MAAM,QAAQ,IAAI,CAAC,MAAO,QAAQ,kBAAAD,QAAG,KAAK,EAAE,MAAM,IAAI,EAAE,MAAO;AAAA,IAC/D,OAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,IAC9B,WAAW,QAAQ,IAAI,CAAC,MAAM,EAAE,YAAY,IAAI;AAAA,IAChD,UAAU;AAAA,EACX,CAAC;AACD,aAAW,OAAO,MAAM;AAEvB,UAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,oBAAoB,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;AAAA,EACjE;AACA,UAAQ,OAAO,MAAM,MAAM,SAAS,IAAI,IAAI;AAC7C;AAEO,SAAS,WAAW,SAAiB,OAAsB;AACjE,QAAM,SAAS,QAAQ,kBAAAA,QAAG,IAAI,QAAQ,IAAI;AAE1C,UAAQ,OAAO,MAAM,GAAG,MAAM,IAAI,oBAAoB,OAAO,CAAC;AAAA,CAAI;AACnE;AAEO,SAAS,UAAU,SAAiB,OAAsB;AAChE,QAAM,SAAS,QAAQ,kBAAAA,QAAG,OAAO,UAAU,IAAI;AAC/C,UAAQ,OAAO,MAAM,GAAG,MAAM,IAAI,oBAAoB,OAAO,CAAC;AAAA,CAAI;AACnE;AAEO,SAAS,IAAI,MAAc,OAAwB;AACzD,SAAO,QAAQ,kBAAAA,QAAG,IAAI,IAAI,IAAI;AAC/B;;;AC1GA,IAAAE,kBAA6B;;;ACD7B,2BAA0C;AAYnC,SAAS,gBAAgB,MAAmC;AAClE,aAAO,qBAAAC,SAAoB;AAAA,IAC1B,SAAS,KAAK;AAAA,IACd,SAAS;AAAA,MACR,mBAAmB,KAAK;AAAA,MACxB,cAAc,eAAe;AAAA,MAC7B,QAAQ;AAAA,IACT;AAAA,IACA,OAAO,CAAC,YAAqB,iBAAiB,SAAS,KAAK,SAAS;AAAA,EACtE,CAAC;AACF;AAEA,SAAS,iBAAiB,SAAkB,WAAsC;AAGjF,QAAM,eAAe,IAAI,QAAQ,SAAS,EAAE,QAAQ,YAAY,QAAQ,SAAS,EAAE,CAAC;AACpF,SAAO,MAAM,YAAY,EAAE,MAAM,CAAC,QAAiB;AAClD,QAAI,eAAe,GAAG,GAAG;AACxB,YAAM,IAAI,aAAa,2BAA2B,SAAS,MAAM,GAAG;AAAA,IACrE;AAEA,UAAM,OAAQ,KAAuC,OAAO;AAC5D,UAAM,SAAS,OAAO,OAAS,KAAe,WAAW,OAAO,GAAG;AACnE,UAAM,IAAI,aAAa,0BAA0B,MAAM,IAAI,GAAG;AAAA,EAC/D,CAAC;AACF;AAEA,SAAS,eAAe,KAAuB;AAC9C,MAAI,OAAO,OAAO,QAAQ,YAAY,UAAU,KAAK;AACpD,UAAM,OAAQ,IAA0B;AACxC,WAAO,SAAS,kBAAkB,SAAS;AAAA,EAC5C;AACA,SAAO;AACR;AAEO,SAAS,iBAAyB;AACxC,SAAO,aAAa,OAAe,UAAU,QAAQ,OAAO,KAAK,QAAQ,QAAQ;AAClF;AAYO,SAAS,mBAAsB,MAAkB;AACvD,MAAI,QAAQ,OAAO,SAAS,YAAY,aAAa,QAAQ,UAAU,MAAM;AAC5E,WAAQ,KAAqB;AAAA,EAC9B;AACA,QAAM,IAAI,SAAS,gEAAgE,SAAS,oBAAoB;AACjH;AAkBO,SAAS,gBAAmB,MAAiC;AACnE,MAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACtC,UAAM,IAAI,SAAS,4CAA4C,SAAS,oBAAoB;AAAA,EAC7F;AACA,QAAM,IAAI;AACV,MAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,GAAG;AAC3B,UAAM,IAAI,SAAS,mDAAmD,SAAS,oBAAoB;AAAA,EACpG;AACA,aAAW,OAAO,CAAC,eAAe,cAAc,YAAY,YAAY,GAAY;AACnF,QAAI,OAAO,EAAE,GAAG,MAAM,UAAU;AAC/B,YAAM,IAAI,SAAS,sDAAsD,GAAG,KAAK,SAAS,oBAAoB;AAAA,IAC/G;AAAA,EACD;AACA,SAAO;AACR;AAEO,SAAS,mBAAmB,QAAgB,MAAsB;AACxE,QAAM,UAAU,oBAAoB,IAAI,KAAK,QAAQ,MAAM;AAC3D,MAAI,UAAU,OAAO,SAAS,KAAK;AAClC,UAAM,IAAI,sBAAsB,SAAS,QAAQ,IAAI;AAAA,EACtD;AACA,QAAM,IAAI,SAAS,SAAS,SAAS,oBAAoB;AAC1D;AAEA,SAAS,oBAAoB,MAA8B;AAC1D,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,QAAQ,OAAO,SAAS,UAAU;AACrC,UAAM,MAAM;AAEZ,QAAI,MAAM,QAAQ,IAAI,OAAO,GAAG;AAC/B,YAAM,QAAQ,IAAI,QAAQ,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AAC1E,UAAI,MAAM,SAAS,EAAG,QAAO,MAAM,KAAK,IAAI;AAAA,IAC7C;AACA,QAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAEhD,QAAI,IAAI,WAAW,OAAO,IAAI,YAAY,UAAU;AACnD,UAAI;AACH,eAAO,KAAK,UAAU,IAAI,OAAO;AAAA,MAClC,QAAQ;AAAA,MAER;AAAA,IACD;AACA,QAAI,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AAAA,EAC/C;AACA,SAAO;AACR;;;ACjIA,qBAUO;AACP,uBAA8B;AAC9B,uBAAqB;AAId,IAAM,kBAAkB,CAAC,SAAS,SAAS,SAAS,SAAS,OAAO;AAGpE,IAAM,iBAAiB,CAAC,SAAS,MAAM;AAU9C,IAAM,cAAc,CAAC,UAAU,gBAAgB,UAAU,YAAY;AAW9D,SAAS,YAAY,KAA+B;AAC1D,SAAQ,YAAkC,SAAS,GAAG;AACvD;AAEA,IAAM,YAAQ,iBAAAC,SAAS,SAAS,EAAE,QAAQ,GAAG,CAAC;AAEvC,SAAS,gBAAwB;AACvC,aAAO,uBAAK,MAAM,QAAQ,aAAa;AACxC;AAOO,SAAS,YAAY,KAAyB;AACpD,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AAC1D,UAAM,IAAI,SAAS,gCAAgC,SAAS,aAAa;AAAA,EAC1E;AACA,QAAM,QAAQ;AACd,QAAM,MAAiB,CAAC;AACxB,aAAW,OAAO,aAAa;AAC9B,QAAI,EAAE,OAAO,OAAQ;AACrB,UAAM,QAAQ,MAAM,GAAG;AACvB,QAAI;AACH,YAAM,UAAU,kBAAkB,KAAK,OAAO,KAAK,CAAC;AAGpD,aAAO,OAAO,KAAK,EAAE,CAAC,GAAG,GAAG,QAAQ,CAAC;AAAA,IACtC,SAAS,KAAK;AACb,UAAI,eAAe,UAAU;AAC5B,cAAM,IAAI,SAAS,eAAe,GAAG,cAAc,IAAI,OAAO,IAAI,SAAS,aAAa;AAAA,MACzF;AACA,YAAM;AAAA,IACP;AAAA,EACD;AACA,SAAO;AACR;AAEO,SAAS,aAAwB;AACvC,QAAM,OAAO,cAAc;AAC3B,MAAI,KAAC,2BAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,MAAI;AACJ,MAAI;AACH,cAAM,6BAAa,MAAM,MAAM;AAAA,EAChC,SAAS,KAAK;AACb,UAAM,IAAI,SAAS,4BAA4B,IAAI,KAAM,IAAc,OAAO,IAAI,SAAS,aAAa;AAAA,EACzG;AACA,MAAI;AACJ,MAAI;AACH,aAAS,KAAK,MAAM,GAAG;AAAA,EACxB,SAAS,KAAK;AACb,UAAM,IAAI,SAAS,2BAA2B,IAAI,KAAM,IAAc,OAAO,IAAI,SAAS,aAAa;AAAA,EACxG;AACA,SAAO,YAAY,MAAM;AAC1B;AAEO,SAAS,WAAW,QAAyB;AACnD,QAAM,OAAO,cAAc;AAC3B,oCAAU,0BAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAI5C,QAAM,UAAU,GAAG,IAAI,QAAQ,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;AACxD,QAAM,SAAK,yBAAS,SAAS,KAAK,GAAK;AACvC,MAAI;AACH,kCAAU,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AAAA,EACrD,SAAS,KAAK;AACb,kCAAU,EAAE;AACZ,QAAI;AACH,qCAAW,OAAO;AAAA,IACnB,QAAQ;AAAA,IAER;AACA,UAAM;AAAA,EACP;AACA,gCAAU,EAAE;AAKZ,MAAI,QAAQ,aAAa,SAAS;AACjC,QAAI;AACH,oCAAU,SAAS,GAAK;AAAA,IACzB,SAAS,KAAK;AACb;AAAA,QACC,2CAA4C,IAAc,OAAO;AAAA,QAEjE;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAEA,iCAAW,SAAS,IAAI;AACzB;AAEO,SAAS,kBAAuC,KAAQ,OAAkC;AAChG,UAAQ,KAAK;AAAA,IACZ,KAAK,UAAU;AACd,UAAI,CAAE,gBAAsC,SAAS,KAAK,GAAG;AAC5D,cAAM,IAAI;AAAA,UACT,mBAAmB,KAAK,eAAe,gBAAgB,KAAK,IAAI,CAAC;AAAA,UACjE,SAAS;AAAA,QACV;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IACA,KAAK,gBAAgB;AACpB,UAAI;AACJ,UAAI;AACH,cAAM,IAAI,IAAI,KAAK;AAAA,MACpB,QAAQ;AACP,cAAM,IAAI,SAAS,gBAAgB,KAAK,IAAI,SAAS,eAAe;AAAA,MACrE;AACA,UAAI,IAAI,aAAa,YAAY,IAAI,aAAa,SAAS;AAC1D,cAAM,IAAI,SAAS,2CAA2C,IAAI,QAAQ,KAAK,SAAS,eAAe;AAAA,MACxG;AACA,aAAO,MAAM,QAAQ,OAAO,EAAE;AAAA,IAC/B;AAAA,IACA,KAAK,UAAU;AACd,UAAI,CAAE,eAAqC,SAAS,KAAK,GAAG;AAC3D,cAAM,IAAI;AAAA,UACT,mBAAmB,KAAK,eAAe,eAAe,KAAK,IAAI,CAAC;AAAA,UAChE,SAAS;AAAA,QACV;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IACA,KAAK,cAAc;AAClB,YAAM,IAAI,OAAO,KAAK;AACtB,UAAI,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,OAAO,IAAI,KAAS;AACnD,cAAM,IAAI;AAAA,UACT,6DAA6D,KAAK;AAAA,UAClE,SAAS;AAAA,QACV;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;;;AC5KA,IAAM,mBAAmB;AACzB,IAAM,iBAAyB;AAC/B,IAAM,qBAAqB;AAmBpB,SAAS,eAAe,SAAmC;AACjE,QAAM,UAAU,QAAQ,gBAAgB;AACxC,QAAM,SAAS,WAAW;AAE1B,SAAO;AAAA,IACN,SAAS,eAAe,QAAQ,KAAK,MAAM;AAAA,IAC3C,QAAQ,cAAc,QAAQ,MAAM,MAAM;AAAA,IAC1C,WAAW,eAAe,QAAQ,SAAS,MAAM;AAAA,IACjD,QAAQ,oBAAoB,QAAQ,MAAM;AAAA,IAC1C,OAAO,eAAe,QAAQ,UAAU,KAAK;AAAA,IAC7C;AAAA,EACD;AACD;AAEA,SAAS,eAAe,UAA8B,QAA2B;AAChF,QAAM,MAAM,YAAY,OAAO,gBAAgB;AAC/C,MAAI;AACH,QAAI,IAAI,GAAG;AAAA,EACZ,QAAQ;AACP,UAAM,IAAI,SAAS,yBAAyB,GAAG,IAAI,SAAS,eAAe;AAAA,EAC5E;AACA,SAAO,IAAI,QAAQ,OAAO,EAAE;AAC7B;AAEA,SAAS,cAAc,UAA8B,QAA2B;AAC/E,QAAM,MAAM,YAAY,OAAO,UAAU;AACzC,MAAI,CAAE,gBAAsC,SAAS,GAAG,GAAG;AAC1D,UAAM,IAAI,SAAS,mBAAmB,GAAG,eAAe,gBAAgB,KAAK,IAAI,CAAC,IAAI,SAAS,eAAe;AAAA,EAC/G;AACA,SAAO;AACR;AAEA,SAAS,eAAe,UAA8B,QAA2B;AAChF,QAAM,MAAM,YAAY,OAAO,cAAc;AAC7C,MAAI,CAAC,OAAO,UAAU,GAAG,KAAK,MAAM,OAAO,MAAM,KAAS;AACzD,UAAM,IAAI,SAAS,qBAAqB,GAAG,kCAAkC,SAAS,eAAe;AAAA,EACtG;AACA,SAAO;AACR;;;AH3BO,SAAS,mBAAmB,QAAuB;AACzD,SACE,QAAQ,QAAQ,EAChB;AAAA,IACA;AAAA,EACD,EACC,OAAO,wBAAwB,4CAA4C,EAC3E,OAAO,4BAA4B,yCAAyC,EAC5E,OAAO,4BAA4B,sCAAsC,EACzE,OAAO,kBAAkB,2BAA2B,CAAC,MAAM,OAAO,CAAC,CAAC,EACpE,OAAO,uBAAuB,mCAAmC,CAAC,MAAM,OAAO,CAAC,CAAC,EACjF,OAAO,uBAAuB,6DAA6D,EAC3F,OAAO,OAAO,OAAoB,YAAqB;AACvD,UAAM,MAAM,eAAe,OAAO;AAClC,UAAM,QAAQ,WAAW,KAAK;AAE9B,UAAM,SAAS,gBAAgB;AAAA,MAC9B,SAAS,IAAI;AAAA,MACb,QAAQ,IAAI;AAAA,MACZ,WAAW,IAAI;AAAA,IAChB,CAAC;AAGD,UAAM,EAAE,MAAM,OAAO,SAAS,IAAI,MAAM,OAAO,IAAI,oBAAoB;AAAA,MACtE,QAAQ,EAAE,MAAM;AAAA,IACjB,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,oBAAmB,SAAS,QAAQ,KAAK;AAE3D,UAAM,QAAQ,gBAA+B,IAAI;AAEjD,QAAI,IAAI,WAAW,QAAQ;AAC1B,gBAAU,KAAK;AACf;AAAA,IACD;AAEA;AAAA,MACC,MAAM;AAAA,MACN;AAAA,QACC,EAAE,QAAQ,UAAU,OAAO,CAAC,MAAM,SAAS,EAAE,UAAU,IAAI,EAAE,EAAE;AAAA,QAC/D,EAAE,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,IAAI,UAAU,GAAG;AAAA,QAC7D,EAAE,QAAQ,WAAW,OAAO,CAAC,MAAM,EAAE,gBAAgB,IAAI,UAAU,GAAG;AAAA,QACtE,EAAE,QAAQ,YAAY,OAAO,CAAC,MAAM,EAAE,gBAAgB,IAAI,UAAU,GAAG;AAAA,QACvE,EAAE,QAAQ,UAAU,OAAO,CAAC,MAAM,EAAE,kBAAkB,IAAI,UAAU,GAAG;AAAA,QACvE,EAAE,QAAQ,WAAW,OAAO,CAAC,MAAM,WAAW,EAAE,UAAU,GAAG,UAAU,GAAG;AAAA,MAC3E;AAAA,MACA,IAAI;AAAA,IACL;AAEA,UAAM,OAAO,gBAAgB,MAAM,WAAW,IAAI,MAAM,UAAU,KAAK,MAAM,KAAK,MAAM,OAAO,MAAM,UAAU;AAC/G,UAAM,OACL,MAAM,aAAa,MAAM,cAAc,oCAAoC,MAAM,cAAc,CAAC,KAAK;AACtG,YAAQ,OAAO,MAAM,IAAI,OAAO,MAAM,IAAI,KAAK,IAAI,IAAI;AAAA,EACxD,CAAC;AACH;AAEA,SAAS,WAAW,OAAiC;AACpD,MAAI,MAAM,WAAW;AAGpB,WAAO,cAAc,MAAM,SAAS;AAAA,EACrC;AACA,QAAM,IAAiB,CAAC;AACxB,MAAI,MAAM,QAAS,GAAE,UAAU,MAAM;AACrC,MAAI,MAAM,UAAU,OAAQ,GAAE,aAAa,MAAM;AACjD,MAAI,MAAM,UAAU,OAAQ,GAAE,2BAA2B,MAAM;AAC/D,MAAI,MAAM,SAAS,OAAW,GAAE,cAAc,MAAM;AACpD,MAAI,MAAM,aAAa,OAAW,GAAE,WAAW,MAAM;AACrD,SAAO;AACR;AAEA,SAAS,cAAc,MAAuC;AAC7D,MAAI;AACJ,MAAI;AACH,cAAM,8BAAa,MAAM,MAAM;AAAA,EAChC,SAAS,KAAK;AACb,UAAM,IAAI,SAAS,iCAAiC,IAAI,KAAM,IAAc,OAAO,IAAI,SAAS,eAAe;AAAA,EAChH;AACA,MAAI;AACH,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACnE,YAAM,IAAI;AAAA,QACT,MAAM,QAAQ,MAAM,IACjB,gEACA;AAAA,MACJ;AAAA,IACD;AACA,WAAO;AAAA,EACR,SAAS,KAAK;AACb,UAAM,IAAI,SAAS,mBAAmB,IAAI,KAAM,IAAc,OAAO,IAAI,SAAS,eAAe;AAAA,EAClG;AACD;AAEA,SAAS,SAAS,GAAW,KAAqB;AAGjD,MAAI,EAAE,UAAU,IAAK,QAAO;AAI5B,QAAM,QAAQ,MAAM,KAAK,CAAC;AAC1B,MAAI,MAAM,UAAU,IAAK,QAAO;AAChC,SAAO,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI;AAC3C;AAEA,SAAS,WAAW,GAA+B;AAClD,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,IAAI,uBAAuB,KAAK,CAAC;AACvC,SAAO,IAAI,EAAE,CAAC,IAAI;AACnB;;;AIlJA,IAAAC,kBAA6B;;;AC+BtB,SAAS,iBAAiB,OAAqB;AACrD,MAAI,QAAQ,MAAM,OAAO;AACxB,UAAM,IAAI,qBAAqB,GAAG,KAAK,gEAAgE;AAAA,EACxG;AACD;;;AD7BA,IAAAC,qBAAe;AAmCR,SAAS,iBAAiB,QAAuB;AACvD,SACE,QAAQ,eAAe,EACvB,YAAY,wDAAwD,EACpE,OAAO,kBAAkB,wEAAwE,EACjG,OAAO,OAAO,UAAkB,OAAkB,YAAqB;AACvE,UAAM,MAAM,eAAe,OAAO;AAClC,UAAM,QAAQ,aAAa,MAAM,UAAU,EAAE,KAAK,IAAI;AACtD,QAAI,CAAC,OAAO;AACX,YAAM,IAAI,SAAS,sBAAsB,SAAS,eAAe;AAAA,IAClE;AAEA,UAAM,SAAS,gBAAgB;AAAA,MAC9B,SAAS,IAAI;AAAA,MACb,QAAQ,IAAI;AAAA,MACZ,WAAW,IAAI;AAAA,IAChB,CAAC;AAED,UAAM,EAAE,MAAM,OAAO,SAAS,IAAI,MAAM,OAAO,IAAI,0BAA0B;AAAA,MAC5E,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE;AAAA,IAC3B,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,oBAAmB,SAAS,QAAQ,KAAK;AAE3D,UAAM,MAAM,mBAA4B,IAAI;AAE5C,QAAI,MAAM,OAAO;AAChB,YAAM,IAAI,QAAQ,KAAK,MAAM,KAAK;AAClC,UAAI,MAAM,QAAW;AACpB,cAAM,IAAI,SAAS,UAAU,MAAM,KAAK,6BAA6B,SAAS,eAAe;AAAA,MAC9F;AAKA,YAAM,MAAM,OAAO,MAAM,WAAW,6BAA6B,CAAC,IAAI,KAAK,UAAU,GAAG,MAAM,CAAC;AAC/F,cAAQ,OAAO,MAAM,MAAM,IAAI;AAC/B;AAAA,IACD;AAEA,QAAI,IAAI,WAAW,QAAQ;AAC1B,gBAAU,GAAG;AACb;AAAA,IACD;AAEA,mBAAe,KAAK,OAAO,IAAI,KAAK;AAAA,EACrC,CAAC;AACH;AAEA,SAAS,eAAe,KAAc,OAAe,OAAsB;AAC1E,QAAM,QAAQ,CAACC,OAAe,QAAQ,mBAAAC,QAAG,KAAKD,EAAC,IAAIA;AAGnD,QAAM,IAAI,CAAC,MAA0C,IAAI,oBAAoB,CAAC,IAAI;AAClF,QAAM,OAAO,IAAI,YAAY,CAAC;AAC9B,QAAM,UAAU,IAAI,uBAAuB,CAAC;AAE5C,QAAM,QAAkB,CAAC;AACzB,MAAI,KAAK,UAAW,OAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,KAAK,SAAS,CAAC,EAAE;AAC7E,MAAI,QAAQ,aAAc,OAAM,KAAK,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,QAAQ,YAAY,CAAC,EAAE;AACzF,MAAI,KAAK,aAAc,OAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,KAAK,YAAY,CAAC,EAAE;AACnF,MAAI,KAAK,eAAgB,OAAM,KAAK,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE,KAAK,cAAc,CAAC,EAAE;AACvF,MAAI,KAAK,oBAAqB,OAAM,KAAK,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,KAAK,mBAAmB,CAAC,EAAE;AACjG,MAAI,KAAK,mBAAoB,OAAM,KAAK,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,KAAK,kBAAkB,CAAC,EAAE;AAE/F,QAAM,KAAK,IAAI,eAAe,EAAE,KAAK,CAAC,IAAI,KAAK,CAAC;AAChD,MAAI,QAAQ,eAAgB,OAAM,KAAK,IAAI,mBAAmB,EAAE,QAAQ,cAAc,CAAC,IAAI,KAAK,CAAC;AACjG,UAAQ,OAAO,MAAM,MAAM,KAAK,IAAI,IAAI,IAAI;AAE5C,MAAI,IAAI,iBAAiB;AACxB,YAAQ,OAAO,MAAM,OAAO,MAAM,aAAa,IAAI,IAAI;AAKvD,YAAQ,OAAO,MAAM,kBAAkB,IAAI,eAAe,IAAI,IAAI;AAAA,EACnE;AAEA,UAAQ,OAAO;AAAA,IACd,OACC;AAAA,MACC;AAAA,MACA;AAAA,IACD,IACA;AAAA,EACF;AACD;AAMA,SAAS,UAAU,GAAmB;AACrC,SAAO,EACL,QAAQ,oCAAoC,IAAI,EAChD,QAAQ,YAAY,EAAE,EACtB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG,EACrB,QAAQ,SAAS,GAAG,EACpB,QAAQ,SAAS,GAAG,EACpB,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG,EACrB,QAAQ,WAAW,MAAM,EACzB,KAAK;AACR;AAEA,SAAS,QAAQ,KAAc,YAA6B;AAC3D,QAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,MAAI,MAAe;AACnB,aAAW,KAAK,OAAO;AACtB,QAAI,OAAO,OAAO,QAAQ,YAAY,OAAO,UAAU,eAAe,KAAK,KAAK,CAAC,GAAG;AACnF,YAAO,IAAgC,CAAC;AAAA,IACzC,OAAO;AACN,aAAO;AAAA,IACR;AAAA,EACD;AACA,SAAO;AACR;AAEA,SAAS,kBAAkB,MAAsB;AAChD,SAAO,6BAA6B,UAAU,IAAI,CAAC;AACpD;AASA,SAAS,YAAoB;AAC5B,mBAAiB,QAAQ;AACzB,MAAI;AACH,eAAO,8BAAa,GAAG,MAAM;AAAA,EAC9B,SAAS,KAAK;AACb,UAAM,IAAI,qBAAqB,qCAAsC,IAAc,OAAO,EAAE;AAAA,EAC7F;AACD;;;AE9KO,SAAS,oBAAoBE,UAAwB;AAC3D,QAAM,OAAOA,SAAQ,QAAQ,MAAM,EAAE,YAAY,qCAAqC;AACtF,qBAAmB,IAAI;AACvB,mBAAiB,IAAI;AACtB;;;ACJO,SAAS,kBAAkB,QAAuB;AACxD,SACE,QAAQ,mBAAmB,EAC3B,YAAY,oEAAoE,EAChF,OAAO,CAAC,KAAa,UAAkB;AACvC,QAAI,CAAC,YAAY,GAAG,GAAG;AACtB,YAAM,IAAI;AAAA,QACT,uBAAuB,GAAG;AAAA,QAC1B,SAAS;AAAA,MACV;AAAA,IACD;AACA,UAAM,UAAU,kBAAkB,KAAK,KAAK;AAC5C,UAAM,SAAS,WAAW;AAG1B,WAAO,OAAO,QAAQ,EAAE,CAAC,GAAG,GAAG,QAAQ,CAAC;AACxC,eAAW,MAAM;AACjB,YAAQ,OAAO,MAAM,OAAO,GAAG,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,CAAI;AAAA,EACjE,CAAC;AACH;;;AClBO,SAAS,kBAAkB,QAAuB;AACxD,SACE,QAAQ,WAAW,EACnB,YAAY,sEAAsE,EAClF,OAAO,CAAC,QAA4B;AACpC,UAAM,SAAS,WAAW;AAC1B,QAAI,QAAQ,QAAW;AACtB,gBAAU,MAAM;AAChB;AAAA,IACD;AACA,QAAI,CAAC,YAAY,GAAG,GAAG;AACtB,YAAM,IAAI;AAAA,QACT,uBAAuB,GAAG;AAAA,QAC1B,SAAS;AAAA,MACV;AAAA,IACD;AACA,UAAM,QAAS,OAAmC,GAAG;AACrD,QAAI,UAAU,QAAW;AACxB,cAAQ,OAAO,MAAM,IAAI;AACzB;AAAA,IACD;AACA,YAAQ,OAAO,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK,KAAK,IAAI;AAAA,EACxF,CAAC;AACH;;;ACzBO,SAAS,mBAAmB,QAAuB;AACzD,SACE,QAAQ,MAAM,EACd,YAAY,qEAAqE,EACjF,OAAO,MAAM;AACb,YAAQ,OAAO,MAAM,cAAc,IAAI,IAAI;AAAA,EAC5C,CAAC;AACH;;;ACTA,IAAAC,kBAAuC;AAIhC,SAAS,oBAAoB,QAAuB;AAC1D,SACE,QAAQ,OAAO,EACf,YAAY,wBAAwB,EACpC,OAAO,eAAe,8BAA8B,EACpD,OAAO,OAAO,SAA8B;AAC5C,UAAM,OAAO,cAAc;AAC3B,QAAI,KAAC,4BAAW,IAAI,GAAG;AACtB,cAAQ,OAAO,MAAM,6BAA6B;AAClD;AAAA,IACD;AACA,QAAI,CAAC,KAAK,OAAO;AAChB,YAAM,KAAK,MAAM,YAAY,oBAAoB,IAAI,UAAU;AAC/D,UAAI,CAAC,IAAI;AACR,gBAAQ,OAAO,MAAM,YAAY;AACjC;AAAA,MACD;AAAA,IACD;AACA,QAAI;AACH,sCAAW,IAAI;AAAA,IAChB,SAAS,KAAK;AACb,YAAM,IAAI;AACV,UAAI,EAAE,SAAS,UAAU;AAExB,gBAAQ,OAAO,MAAM,+CAA+C;AACpE;AAAA,MACD;AACA,YAAM,IAAI,qBAAqB,8BAA8B,IAAI,KAAK,EAAE,OAAO,EAAE;AAAA,IAClF;AACA,YAAQ,OAAO,MAAM,WAAW,IAAI;AAAA,CAAI;AAAA,EACzC,CAAC;AACH;AAEA,SAAS,YAAY,QAAkC;AACtD,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC/B,YAAQ,OAAO,MAAM,MAAM;AAC3B,QAAI,MAAM;AACV,YAAQ,MAAM,YAAY,MAAM;AAChC,UAAM,SAAS,CAAC,UAAkB;AACjC,aAAO;AACP,YAAM,KAAK,IAAI,QAAQ,IAAI;AAC3B,UAAI,MAAM,GAAG;AACZ,gBAAQ;AACR,cAAM,SAAS,IAAI,MAAM,GAAG,EAAE,EAAE,KAAK,EAAE,YAAY;AACnD,gBAAQ,WAAW,OAAO,WAAW,KAAK;AAAA,MAC3C;AAAA,IACD;AACA,UAAM,QAAQ,MAAM;AACnB,cAAQ;AACR,cAAQ,OAAO,MAAM,gCAA2B;AAChD,cAAQ,KAAK;AAAA,IACd;AACA,UAAM,UAAU,MAAM;AACrB,cAAQ,MAAM,eAAe,QAAQ,MAAM;AAC3C,cAAQ,MAAM,eAAe,OAAO,KAAK;AACzC,cAAQ,MAAM,MAAM;AAAA,IACrB;AACA,YAAQ,MAAM,GAAG,QAAQ,MAAM;AAC/B,YAAQ,MAAM,GAAG,OAAO,KAAK;AAAA,EAC9B,CAAC;AACF;;;AC3DO,SAAS,sBAAsBC,UAAwB;AAC7D,QAAM,SAASA,SAAQ,QAAQ,QAAQ,EAAE,YAAY,0BAA0B;AAC/E,oBAAkB,MAAM;AACxB,oBAAkB,MAAM;AACxB,qBAAmB,MAAM;AACzB,sBAAoB,MAAM;AAC3B;;;AdNA,IAAM,UAAU,IAAI,yBAAQ;AAE5B,QACE,KAAK,OAAO,EACZ,YAAY,8EAAyE,EACrF,QAAQ,SAAiB,iBAAiB,wBAAwB,EAClE,OAAO,mBAAmB,+DAA+D,EACzF,OAAO,eAAe,uBAAuB,EAC7C,OAAO,kBAAkB,6BAA6B,EACtD,OAAO,cAAc,sBAAsB,EAC3C,OAAO,kBAAkB,gCAAgC,CAAC,MAAM,OAAO,CAAC,CAAC;AAE3E,oBAAoB,OAAO;AAC3B,sBAAsB,OAAO;AAE7B,QAAQ,aAAa;AAErB,QACE,WAAW,QAAQ,IAAI,EACvB,KAAK,MAAM,QAAQ,KAAK,SAAS,OAAO,CAAC,EACzC,MAAM,CAAC,QAAiB,oBAAoB,GAAG,CAAC;AAElD,SAAS,oBAAoB,KAAqB;AACjD,QAAM,QAAQ,eAAe,KAAK;AAGlC,MAAI,OAAO,OAAO,QAAQ,YAAY,UAAU,KAAK;AACpD,UAAM,eAAe;AACrB,QAAI,aAAa,SAAS,6BAA6B,aAAa,SAAS,qBAAqB;AACjG,cAAQ,KAAK,SAAS,OAAO;AAAA,IAC9B;AAGA,QAAI,aAAa,QAAS,YAAW,aAAa,SAAS,KAAK;AAChE,YAAQ,KAAK,SAAS,eAAe;AAAA,EACtC;AAEA,MAAI,WAAW,GAAG,GAAG;AACpB,eAAW,IAAI,SAAS,KAAK;AAC7B,YAAQ,KAAK,IAAI,QAAQ;AAAA,EAC1B;AAEA,QAAM,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACvE,aAAW,iBAAiB,KAAK;AACjC,UAAQ,KAAK,SAAS,oBAAoB;AAC3C;","names":["pc","Table","import_node_fs","createClient","envPaths","import_node_fs","import_picocolors","s","pc","program","import_node_fs","program"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wport/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "wport CLI — terminal interface to the W101 Talent Search Hub public API",
|
|
5
|
+
"bin": {
|
|
6
|
+
"wport": "bin/wport.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.17"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"dist/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"gen:openapi": "openapi-typescript ../../docs/swagger/swagger-dev.json -o src/generated/schema.d.ts",
|
|
21
|
+
"gen:openapi:remote": "node scripts/gen-from-remote.cjs http://localhost:3000/api/api-doc-json src/generated/schema.d.ts",
|
|
22
|
+
"gen:openapi:prod": "node scripts/gen-from-remote.cjs https://api.wport.me/api/api-doc-json src/generated/schema.d.ts",
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"start": "node dist/index.js",
|
|
26
|
+
"test": "vitest run --passWithNoTests",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"lint": "npm run typecheck",
|
|
29
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"wport",
|
|
36
|
+
"w101",
|
|
37
|
+
"jobs",
|
|
38
|
+
"cli",
|
|
39
|
+
"talent",
|
|
40
|
+
"job-search"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"cli-table3": "^0.6.5",
|
|
45
|
+
"commander": "^12.1.0",
|
|
46
|
+
"env-paths": "^3.0.0",
|
|
47
|
+
"openapi-fetch": "^0.12.0",
|
|
48
|
+
"ora": "^8.1.0",
|
|
49
|
+
"picocolors": "^1.0.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.14.0",
|
|
53
|
+
"openapi-typescript": "^7.4.0",
|
|
54
|
+
"tsup": "^8.3.0",
|
|
55
|
+
"typescript": "^5.5.0",
|
|
56
|
+
"vitest": "^2.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|