@wport/cli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -4
- package/README.md +28 -0
- package/dist/index.js +906 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -179,7 +179,7 @@ function isTimeoutAbort(err) {
|
|
|
179
179
|
return false;
|
|
180
180
|
}
|
|
181
181
|
function buildUserAgent() {
|
|
182
|
-
return `wport-cli/${"0.
|
|
182
|
+
return `wport-cli/${"0.6.0"} (node ${process.version}; ${process.platform})`;
|
|
183
183
|
}
|
|
184
184
|
function unwrapDataResponse(body) {
|
|
185
185
|
if (body && typeof body === "object" && "success" in body && "data" in body) {
|
|
@@ -663,6 +663,25 @@ function readJsonInput(source, options = {}) {
|
|
|
663
663
|
throw new InvalidArgumentError("Invalid JSON in input (parse failed)");
|
|
664
664
|
}
|
|
665
665
|
}
|
|
666
|
+
function readTextInput(source, options = {}) {
|
|
667
|
+
const readStdin = options.readStdin ?? ((label) => readPipedStdin(label, { timeoutMs: options.timeoutMs }));
|
|
668
|
+
let raw;
|
|
669
|
+
if (source === "-") {
|
|
670
|
+
raw = readStdin("--body-file -");
|
|
671
|
+
} else {
|
|
672
|
+
try {
|
|
673
|
+
raw = (0, import_node_fs3.readFileSync)(source, "utf8");
|
|
674
|
+
} catch (err) {
|
|
675
|
+
throw new InvalidArgumentError(
|
|
676
|
+
`Cannot read --body-file "${source}": ${err.code ?? err.message}`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!raw.trim()) {
|
|
681
|
+
throw new InvalidArgumentError("Input is empty \u2014 expected message body text");
|
|
682
|
+
}
|
|
683
|
+
return raw;
|
|
684
|
+
}
|
|
666
685
|
function readJsonObject(source, options = {}) {
|
|
667
686
|
const parsed = readJsonInput(source, options);
|
|
668
687
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -953,13 +972,20 @@ function registerConfigCommand(program2) {
|
|
|
953
972
|
// src/commands/doctor.ts
|
|
954
973
|
var import_node_fs5 = require("fs");
|
|
955
974
|
var SILENT_IGNORED_PARAMS = ["orderBy", "order"];
|
|
975
|
+
var ENTERPRISE_TALENT_BOUNDARY_NOTES = [
|
|
976
|
+
"`enterprise talents` covers your APPLIED pool only (list / view / respond).",
|
|
977
|
+
"The visit tab is not available yet: `talents list --tab visit` returns 400.",
|
|
978
|
+
"There is NO active candidate search over an API Key. `/api/search-talent` needs an",
|
|
979
|
+
"interactive JWT (company-mode) session and returns 401 for an API Key \u2014 by design,",
|
|
980
|
+
"not a setup error. Do not script talent-sourcing against an Enterprise API Key."
|
|
981
|
+
];
|
|
956
982
|
function registerDoctorCommand(program2) {
|
|
957
983
|
program2.command("doctor").description(
|
|
958
984
|
"Diagnose CLI setup: resolved config, server reachability, schema fingerprint, and known server quirks."
|
|
959
985
|
).action(async (_opts, command) => {
|
|
960
986
|
const ctx = resolveContext(command);
|
|
961
987
|
const line = (s = "") => process.stdout.write(s + "\n");
|
|
962
|
-
line(`wport-cli ${"0.
|
|
988
|
+
line(`wport-cli ${"0.6.0"}`);
|
|
963
989
|
line(` bundled schema fingerprint: ${"839e8a891dfb"}`);
|
|
964
990
|
line("");
|
|
965
991
|
line("Resolved configuration:");
|
|
@@ -979,6 +1005,9 @@ function registerDoctorCommand(program2) {
|
|
|
979
1005
|
line(" \u2022 jobs search sorts by publish date, or by relevance when --keyword is set.");
|
|
980
1006
|
line(" \u2022 jobs view --batch caps parallelism (default 5, max 20) to stay friendly to the API.");
|
|
981
1007
|
line("");
|
|
1008
|
+
line("Enterprise talent scope (Enterprise API Key):");
|
|
1009
|
+
for (const note of ENTERPRISE_TALENT_BOUNDARY_NOTES) line(` \u2022 ${note}`);
|
|
1010
|
+
line("");
|
|
982
1011
|
line("Schema drift:");
|
|
983
1012
|
line(" The fingerprint above identifies the OpenAPI contract this CLI was built against.");
|
|
984
1013
|
line(" Automated drift detection needs a server-side schema-version endpoint, which is");
|
|
@@ -1068,8 +1097,25 @@ function throwEnterpriseHttpError(status, body) {
|
|
|
1068
1097
|
ExitCode.ServerClientError
|
|
1069
1098
|
);
|
|
1070
1099
|
}
|
|
1100
|
+
if (status === 400) {
|
|
1101
|
+
const missingFields = extractMissingFields(body);
|
|
1102
|
+
if (missingFields.length > 0) {
|
|
1103
|
+
throw new CliError(
|
|
1104
|
+
`${base} \u2014 Missing required fields: ${missingFields.join(", ")}. Fill them via \`wport enterprise jobs update <enc_id> ...\` (or the web console), then publish.`,
|
|
1105
|
+
ExitCode.ServerClientError
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1071
1109
|
throwForHttpStatus(status, body);
|
|
1072
1110
|
}
|
|
1111
|
+
function extractMissingFields(body) {
|
|
1112
|
+
if (!body || typeof body !== "object") return [];
|
|
1113
|
+
const data = body.data;
|
|
1114
|
+
if (!data || typeof data !== "object") return [];
|
|
1115
|
+
const fields = data.missing_fields;
|
|
1116
|
+
if (!Array.isArray(fields)) return [];
|
|
1117
|
+
return fields.filter((f) => typeof f === "string");
|
|
1118
|
+
}
|
|
1073
1119
|
function warnIfRateLimitLow(headers) {
|
|
1074
1120
|
const remaining = Number(headers.get("x-ratelimit-remaining"));
|
|
1075
1121
|
const limit = Number(headers.get("x-ratelimit-limit"));
|
|
@@ -1251,6 +1297,41 @@ function registerEnterpriseWhoami(parent) {
|
|
|
1251
1297
|
});
|
|
1252
1298
|
}
|
|
1253
1299
|
|
|
1300
|
+
// src/commands/enterprise/usage.ts
|
|
1301
|
+
function num(value) {
|
|
1302
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "\u2014";
|
|
1303
|
+
}
|
|
1304
|
+
async function runUsage(ctx, apiKey) {
|
|
1305
|
+
const { body } = await enterpriseGet(
|
|
1306
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1307
|
+
"/usage"
|
|
1308
|
+
);
|
|
1309
|
+
const usage = unwrapDataResponse(body);
|
|
1310
|
+
if (ctx.format === "json") {
|
|
1311
|
+
printJson(usage);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const quota = usage.quota ?? {};
|
|
1315
|
+
const rate = usage.rate_limit ?? {};
|
|
1316
|
+
const lines = [
|
|
1317
|
+
`period: ${usage.period ?? "\u2014"}`,
|
|
1318
|
+
`monthly quota: ${num(quota.used)} / ${num(quota.limit)} used (${num(quota.remaining)} remaining)`,
|
|
1319
|
+
`rate limit: ${num(rate.limit)} requests / ${num(rate.window_seconds)}s per key`
|
|
1320
|
+
];
|
|
1321
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1322
|
+
process.stdout.write(
|
|
1323
|
+
dim("Live per-window rate-limit headroom is reported via response headers on write requests.", ctx.color) + "\n"
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
function registerEnterpriseUsage(parent) {
|
|
1327
|
+
parent.command("usage").description("Show this month API quota usage and rate-limit ceiling").action(async (_flags, command) => {
|
|
1328
|
+
const ctx = resolveContext(command);
|
|
1329
|
+
const globals = command.optsWithGlobals();
|
|
1330
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1331
|
+
await runUsage(ctx, key);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1254
1335
|
// src/commands/enterprise/jobs/list.ts
|
|
1255
1336
|
var STATUS_MAP = { published: 1, unpublished: 0 };
|
|
1256
1337
|
var MINIMAL_LIST_FIELDS = ["enc_id", "job_title", "status", "updated_at"];
|
|
@@ -1270,44 +1351,51 @@ function formatStatus(status) {
|
|
|
1270
1351
|
function formatDate2(value) {
|
|
1271
1352
|
return value ? String(value).slice(0, 10) : "";
|
|
1272
1353
|
}
|
|
1354
|
+
function formatCount(value) {
|
|
1355
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "\u2014";
|
|
1356
|
+
}
|
|
1357
|
+
async function runEnterpriseJobsList(ctx, apiKey, flags) {
|
|
1358
|
+
if (flags.fields && flags.minimal) {
|
|
1359
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
1360
|
+
}
|
|
1361
|
+
const { body } = await enterpriseGet(
|
|
1362
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1363
|
+
"/jobs",
|
|
1364
|
+
{
|
|
1365
|
+
currentPage: flags.page,
|
|
1366
|
+
pageSize: flags.pageSize,
|
|
1367
|
+
keyword: flags.keyword,
|
|
1368
|
+
status: mapStatusFlag(flags.status)
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
const paged = asPaginatedBody(body);
|
|
1372
|
+
const projection = flags.minimal ? MINIMAL_LIST_FIELDS : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
1373
|
+
if (projection || ctx.format === "json") {
|
|
1374
|
+
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
printTable(
|
|
1378
|
+
paged.data,
|
|
1379
|
+
[
|
|
1380
|
+
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
1381
|
+
{ header: "TITLE", value: (r) => r.job_title ?? "", maxWidth: 36 },
|
|
1382
|
+
{ header: "STATUS", value: (r) => formatStatus(r.status) },
|
|
1383
|
+
{ header: "CLICKS_7D", value: (r) => formatCount(r.clicks_7d) },
|
|
1384
|
+
{ header: "PUBLISHED", value: (r) => formatDate2(r.published_at), maxWidth: 12 },
|
|
1385
|
+
{ header: "UPDATED", value: (r) => formatDate2(r.updated_at), maxWidth: 12 }
|
|
1386
|
+
],
|
|
1387
|
+
ctx.color
|
|
1388
|
+
);
|
|
1389
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} jobs).`;
|
|
1390
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise jobs list --page ${paged.currentPage + 1}` : "";
|
|
1391
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
1392
|
+
}
|
|
1273
1393
|
function registerEnterpriseJobsList(parent) {
|
|
1274
1394
|
parent.command("list").description("List your company job postings").option("--page <n>", "page number (server: currentPage, default 1)", (v) => Number(v)).option("--page-size <n>", "items per page (server: pageSize, default 10, max 100)", (v) => Number(v)).option("--keyword <kw>", "filter by job title keyword").option("--status <state>", "filter by status: published | unpublished").option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").option("--minimal", `output only ${MINIMAL_LIST_FIELDS.join(",")} as JSON`).action(async (flags, command) => {
|
|
1275
1395
|
const ctx = resolveContext(command);
|
|
1276
|
-
if (flags.fields && flags.minimal) {
|
|
1277
|
-
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
1278
|
-
}
|
|
1279
1396
|
const globals = command.optsWithGlobals();
|
|
1280
1397
|
const { key } = resolveApiKey(globals.apiKey);
|
|
1281
|
-
|
|
1282
|
-
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey: key },
|
|
1283
|
-
"/jobs",
|
|
1284
|
-
{
|
|
1285
|
-
currentPage: flags.page,
|
|
1286
|
-
pageSize: flags.pageSize,
|
|
1287
|
-
keyword: flags.keyword,
|
|
1288
|
-
status: mapStatusFlag(flags.status)
|
|
1289
|
-
}
|
|
1290
|
-
);
|
|
1291
|
-
const paged = asPaginatedBody(body);
|
|
1292
|
-
const projection = flags.minimal ? MINIMAL_LIST_FIELDS : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
1293
|
-
if (projection || ctx.format === "json") {
|
|
1294
|
-
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
printTable(
|
|
1298
|
-
paged.data,
|
|
1299
|
-
[
|
|
1300
|
-
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
1301
|
-
{ header: "TITLE", value: (r) => r.job_title ?? "", maxWidth: 36 },
|
|
1302
|
-
{ header: "STATUS", value: (r) => formatStatus(r.status) },
|
|
1303
|
-
{ header: "CODE", value: (r) => r.code ?? "", maxWidth: 16 },
|
|
1304
|
-
{ header: "UPDATED", value: (r) => formatDate2(r.updated_at), maxWidth: 12 }
|
|
1305
|
-
],
|
|
1306
|
-
ctx.color
|
|
1307
|
-
);
|
|
1308
|
-
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} jobs).`;
|
|
1309
|
-
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise jobs list --page ${paged.currentPage + 1}` : "";
|
|
1310
|
-
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
1398
|
+
await runEnterpriseJobsList(ctx, key, flags);
|
|
1311
1399
|
});
|
|
1312
1400
|
}
|
|
1313
1401
|
|
|
@@ -1449,6 +1537,67 @@ async function runJobsDelete(ctx, apiKey, encId, confirm, idempotencyKey) {
|
|
|
1449
1537
|
process.stdout.write(`Deleted job: ${trimmed}
|
|
1450
1538
|
`);
|
|
1451
1539
|
}
|
|
1540
|
+
async function runJobsCopy(ctx, apiKey, encId, idempotencyKey) {
|
|
1541
|
+
const trimmed = requireEncId(encId);
|
|
1542
|
+
const { body } = await enterprisePost(
|
|
1543
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1544
|
+
`/jobs/${encodeURIComponent(trimmed)}/copy`,
|
|
1545
|
+
{},
|
|
1546
|
+
{ idempotencyKey }
|
|
1547
|
+
);
|
|
1548
|
+
const result = unwrapDataResponse(body);
|
|
1549
|
+
if (ctx.format === "json") {
|
|
1550
|
+
printJson(result);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
process.stdout.write(`Copied job ${trimmed} \u2192 new draft: ${result.enc_id ?? "(unknown)"}
|
|
1554
|
+
`);
|
|
1555
|
+
const incomplete = extractStringArray(result.incomplete_fields);
|
|
1556
|
+
if (incomplete.length > 0) {
|
|
1557
|
+
process.stdout.write(` \u26A0 Incomplete for publish \u2014 fill before \`jobs publish\`: ${incomplete.join(", ")}
|
|
1558
|
+
`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function extractStringArray(value) {
|
|
1562
|
+
if (!Array.isArray(value)) return [];
|
|
1563
|
+
return value.filter((v) => typeof v === "string");
|
|
1564
|
+
}
|
|
1565
|
+
function registerEnterpriseJobsCopy(parent) {
|
|
1566
|
+
parent.command("copy <enc_id>").description("Copy a job posting into a new unpublished draft").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
1567
|
+
const ctx = resolveContext(command);
|
|
1568
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1569
|
+
await runJobsCopy(ctx, key, encId, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
async function runJobsClose(ctx, apiKey, encId, confirm, idempotencyKey) {
|
|
1573
|
+
if (!confirm) {
|
|
1574
|
+
throw new CliError(
|
|
1575
|
+
"Refusing to close without --confirm (irreversible; a closed job cannot be reopened \u2014 copy it to relist)",
|
|
1576
|
+
ExitCode.InvalidArgument
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
const trimmed = requireEncId(encId);
|
|
1580
|
+
const { body } = await enterprisePatch(
|
|
1581
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1582
|
+
`/jobs/${encodeURIComponent(trimmed)}/close`,
|
|
1583
|
+
{},
|
|
1584
|
+
{ idempotencyKey }
|
|
1585
|
+
);
|
|
1586
|
+
const result = unwrapDataResponse(body);
|
|
1587
|
+
if (ctx.format === "json") {
|
|
1588
|
+
printJson(result);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
process.stdout.write(`Closed job: ${result.enc_id ?? trimmed}
|
|
1592
|
+
`);
|
|
1593
|
+
}
|
|
1594
|
+
function registerEnterpriseJobsClose(parent) {
|
|
1595
|
+
parent.command("close <enc_id>").description("Close (finalize) a job posting (destructive, irreversible; requires --confirm)").option("--confirm", "confirm this irreversible close (a closed job cannot be reopened)").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
1596
|
+
const ctx = resolveContext(command);
|
|
1597
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1598
|
+
await runJobsClose(ctx, key, encId, flags.confirm === true, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1452
1601
|
function registerEnterpriseJobsPublish(parent) {
|
|
1453
1602
|
parent.command("publish <enc_id>").description("Publish a job posting").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
1454
1603
|
const ctx = resolveContext(command);
|
|
@@ -1530,6 +1679,8 @@ function registerEnterpriseJobsCommand(parent) {
|
|
|
1530
1679
|
registerEnterpriseJobsPublish(jobs);
|
|
1531
1680
|
registerEnterpriseJobsUnpublish(jobs);
|
|
1532
1681
|
registerEnterpriseJobsDelete(jobs);
|
|
1682
|
+
registerEnterpriseJobsCopy(jobs);
|
|
1683
|
+
registerEnterpriseJobsClose(jobs);
|
|
1533
1684
|
registerEnterpriseJobsBatch(jobs);
|
|
1534
1685
|
}
|
|
1535
1686
|
|
|
@@ -1636,19 +1787,738 @@ function registerEnterpriseKeysCommand(parent) {
|
|
|
1636
1787
|
registerEnterpriseKeysRotate(keys);
|
|
1637
1788
|
}
|
|
1638
1789
|
|
|
1790
|
+
// src/commands/enterprise/company/view.ts
|
|
1791
|
+
var COMPANY_STATUS_LABELS = {
|
|
1792
|
+
0: "not_submitted",
|
|
1793
|
+
1: "pending_review",
|
|
1794
|
+
2: "approved",
|
|
1795
|
+
3: "rejected"
|
|
1796
|
+
};
|
|
1797
|
+
function formatCompanyStatus(status) {
|
|
1798
|
+
if (status === void 0) return "";
|
|
1799
|
+
return COMPANY_STATUS_LABELS[status] ?? String(status);
|
|
1800
|
+
}
|
|
1801
|
+
function formatPhone(company) {
|
|
1802
|
+
const code = company.phone_code ?? void 0;
|
|
1803
|
+
const number = company.phone_number ?? void 0;
|
|
1804
|
+
if (!code && !number) return void 0;
|
|
1805
|
+
return [code, number].filter(Boolean).join(" ");
|
|
1806
|
+
}
|
|
1807
|
+
function formatCapital(company) {
|
|
1808
|
+
if (company.capital_amount === null || company.capital_amount === void 0) return void 0;
|
|
1809
|
+
if (company.capital_show_status === 0) return "not displayed";
|
|
1810
|
+
return String(company.capital_amount);
|
|
1811
|
+
}
|
|
1812
|
+
function formatAddress(company) {
|
|
1813
|
+
return company.address ?? void 0;
|
|
1814
|
+
}
|
|
1815
|
+
var DETAIL_FIELDS2 = [
|
|
1816
|
+
"name",
|
|
1817
|
+
"uniform_number",
|
|
1818
|
+
"status",
|
|
1819
|
+
"website",
|
|
1820
|
+
"phone",
|
|
1821
|
+
"address",
|
|
1822
|
+
"capital",
|
|
1823
|
+
"logo_url"
|
|
1824
|
+
];
|
|
1825
|
+
function renderDetailLines2(company) {
|
|
1826
|
+
const pad = Math.max(...DETAIL_FIELDS2.map((f) => f.length)) + 1;
|
|
1827
|
+
const derived = {
|
|
1828
|
+
name: company.name,
|
|
1829
|
+
uniform_number: company.uniform_number ?? void 0,
|
|
1830
|
+
status: formatCompanyStatus(company.status),
|
|
1831
|
+
website: company.website ?? void 0,
|
|
1832
|
+
phone: formatPhone(company),
|
|
1833
|
+
address: formatAddress(company),
|
|
1834
|
+
capital: formatCapital(company),
|
|
1835
|
+
logo_url: company.logo_url ?? void 0
|
|
1836
|
+
};
|
|
1837
|
+
const lines = [];
|
|
1838
|
+
for (const field of DETAIL_FIELDS2) {
|
|
1839
|
+
const value = derived[field];
|
|
1840
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
1841
|
+
lines.push(`${(field + ":").padEnd(pad + 1)}${sanitizeForTerminal(value)}`);
|
|
1842
|
+
}
|
|
1843
|
+
return lines;
|
|
1844
|
+
}
|
|
1845
|
+
async function runCompanyView(ctx, apiKey, flags) {
|
|
1846
|
+
const { body } = await enterpriseGet(
|
|
1847
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1848
|
+
"/company"
|
|
1849
|
+
);
|
|
1850
|
+
const company = unwrapDataResponse(body);
|
|
1851
|
+
if (flags.fields) {
|
|
1852
|
+
printJson(pickPaths(company, parseFieldsList(flags.fields)));
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (ctx.format === "json") {
|
|
1856
|
+
printJson(company);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
process.stdout.write(renderDetailLines2(company).join("\n") + "\n");
|
|
1860
|
+
}
|
|
1861
|
+
function registerEnterpriseCompanyView(parent) {
|
|
1862
|
+
parent.command("view").description("View your company information").option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").action(async (flags, command) => {
|
|
1863
|
+
const ctx = resolveContext(command);
|
|
1864
|
+
const globals = command.optsWithGlobals();
|
|
1865
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1866
|
+
await runCompanyView(ctx, key, flags);
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/commands/enterprise/company/update.ts
|
|
1871
|
+
var import_node_crypto5 = require("crypto");
|
|
1872
|
+
|
|
1873
|
+
// src/commands/enterprise/company/types.ts
|
|
1874
|
+
var BASIC_FIELDS = [
|
|
1875
|
+
"name",
|
|
1876
|
+
"industry_category_code",
|
|
1877
|
+
"phone_code",
|
|
1878
|
+
"phone_number",
|
|
1879
|
+
"employee_count_range_code",
|
|
1880
|
+
"capital_amount",
|
|
1881
|
+
"capital_show_status",
|
|
1882
|
+
"website",
|
|
1883
|
+
"area_code",
|
|
1884
|
+
"address"
|
|
1885
|
+
];
|
|
1886
|
+
var DESCRIPTION_FIELDS = ["description", "products_services", "latest_news"];
|
|
1887
|
+
var FORBIDDEN_FIELDS = [
|
|
1888
|
+
"uniform_number",
|
|
1889
|
+
"banner_url",
|
|
1890
|
+
"photo_1",
|
|
1891
|
+
"photo_2",
|
|
1892
|
+
"photo_3",
|
|
1893
|
+
"video_1",
|
|
1894
|
+
"video_2",
|
|
1895
|
+
"video_3",
|
|
1896
|
+
"foundation_date",
|
|
1897
|
+
"representative",
|
|
1898
|
+
"directors",
|
|
1899
|
+
"milestones",
|
|
1900
|
+
"awards",
|
|
1901
|
+
"qa"
|
|
1902
|
+
];
|
|
1903
|
+
|
|
1904
|
+
// src/commands/enterprise/company/update.ts
|
|
1905
|
+
var REQUIRED_BASIC_FIELDS = [
|
|
1906
|
+
"name",
|
|
1907
|
+
"industry_category_code",
|
|
1908
|
+
"phone_code",
|
|
1909
|
+
"phone_number",
|
|
1910
|
+
"area_code",
|
|
1911
|
+
"address"
|
|
1912
|
+
];
|
|
1913
|
+
var CONTRACT_REQUIRED_GET_FIELDS = [
|
|
1914
|
+
"industry_category_code",
|
|
1915
|
+
"area_code",
|
|
1916
|
+
"employee_count_range_code",
|
|
1917
|
+
"capital_amount",
|
|
1918
|
+
"capital_show_status",
|
|
1919
|
+
"latest_news",
|
|
1920
|
+
"uniform_number"
|
|
1921
|
+
];
|
|
1922
|
+
async function buildCompanyUpdatePayloads(input, ctx, apiKey) {
|
|
1923
|
+
const forbiddenFound = Object.keys(input).filter((k) => FORBIDDEN_FIELDS.includes(k));
|
|
1924
|
+
if (forbiddenFound.length > 0) {
|
|
1925
|
+
throw new InvalidArgumentError(`Field(s) not writable via CLI: ${forbiddenFound.join(", ")}`);
|
|
1926
|
+
}
|
|
1927
|
+
const writableFields = /* @__PURE__ */ new Set([...BASIC_FIELDS, ...DESCRIPTION_FIELDS]);
|
|
1928
|
+
const unknownFound = Object.keys(input).filter((k) => !writableFields.has(k));
|
|
1929
|
+
if (unknownFound.length > 0) {
|
|
1930
|
+
throw new InvalidArgumentError(`Unknown field(s): ${unknownFound.join(", ")}`);
|
|
1931
|
+
}
|
|
1932
|
+
const inputHasBasicField = BASIC_FIELDS.some((f) => f in input);
|
|
1933
|
+
const inputHasDescriptionField = DESCRIPTION_FIELDS.some((f) => f in input);
|
|
1934
|
+
if (!inputHasBasicField && !inputHasDescriptionField) {
|
|
1935
|
+
throw new InvalidArgumentError("No writable company fields provided");
|
|
1936
|
+
}
|
|
1937
|
+
const { body } = await enterpriseGet(
|
|
1938
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1939
|
+
"/company"
|
|
1940
|
+
);
|
|
1941
|
+
const current = unwrapDataResponse(body);
|
|
1942
|
+
const missingContractFields = CONTRACT_REQUIRED_GET_FIELDS.filter((f) => !(f in current));
|
|
1943
|
+
if (missingContractFields.length > 0) {
|
|
1944
|
+
throw new CliError(
|
|
1945
|
+
`Backend contract appears broken: GET /company response is missing field(s): ${missingContractFields.join(", ")}`,
|
|
1946
|
+
ExitCode.ServerOrNetworkError
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
let basic = null;
|
|
1950
|
+
if (inputHasBasicField) {
|
|
1951
|
+
const merged = {};
|
|
1952
|
+
for (const field of BASIC_FIELDS) {
|
|
1953
|
+
merged[field] = field in input ? input[field] : current[field];
|
|
1954
|
+
}
|
|
1955
|
+
const missingRequired = REQUIRED_BASIC_FIELDS.filter((f) => {
|
|
1956
|
+
const v = merged[f];
|
|
1957
|
+
return v === null || v === void 0 || v === "";
|
|
1958
|
+
});
|
|
1959
|
+
if (missingRequired.length > 0) {
|
|
1960
|
+
const [first] = missingRequired;
|
|
1961
|
+
throw new InvalidArgumentError(
|
|
1962
|
+
`Missing required field '${first}' \u2014 not provided in input and not present on your current company profile` + (missingRequired.length > 1 ? ` (also missing: ${missingRequired.slice(1).join(", ")})` : "")
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
basic = merged;
|
|
1966
|
+
}
|
|
1967
|
+
let descriptions = null;
|
|
1968
|
+
if (inputHasDescriptionField) {
|
|
1969
|
+
descriptions = {};
|
|
1970
|
+
for (const field of DESCRIPTION_FIELDS) {
|
|
1971
|
+
if (field in input) descriptions[field] = input[field];
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return { basic, descriptions };
|
|
1975
|
+
}
|
|
1976
|
+
function isDescriptionsPartialFailureBody(body) {
|
|
1977
|
+
return !!body && typeof body === "object" && Array.isArray(body.updated_sections) && typeof body.failed_section === "string";
|
|
1978
|
+
}
|
|
1979
|
+
function printCompanyResult(company, format) {
|
|
1980
|
+
if (format === "json") {
|
|
1981
|
+
printJson(company);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
process.stdout.write(`Updated company: ${company.name ?? ""}
|
|
1985
|
+
`);
|
|
1986
|
+
}
|
|
1987
|
+
function resolveUpdateIdempotencyKeys(needsBasic, needsDescriptions, flags) {
|
|
1988
|
+
const bothNeeded = needsBasic && needsDescriptions;
|
|
1989
|
+
if (bothNeeded && flags.idempotencyKey !== void 0) {
|
|
1990
|
+
throw new InvalidArgumentError(
|
|
1991
|
+
"Both basic and descriptions sections changed: use --basic-idempotency-key and --descriptions-idempotency-key instead of --idempotency-key, to avoid reusing the same key for two different payloads"
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
basicKey: needsBasic ? flags.basicIdempotencyKey ?? flags.idempotencyKey ?? (0, import_node_crypto5.randomUUID)() : void 0,
|
|
1996
|
+
descriptionsKey: needsDescriptions ? flags.descriptionsIdempotencyKey ?? flags.idempotencyKey ?? (0, import_node_crypto5.randomUUID)() : void 0
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
async function runCompanyUpdate(ctx, apiKey, source, idempotencyFlags, options = {}) {
|
|
2000
|
+
const input = readJsonObject(source, options);
|
|
2001
|
+
const needsBasic = BASIC_FIELDS.some((f) => f in input);
|
|
2002
|
+
const needsDescriptions = DESCRIPTION_FIELDS.some((f) => f in input);
|
|
2003
|
+
const { basicKey, descriptionsKey } = resolveUpdateIdempotencyKeys(needsBasic, needsDescriptions, idempotencyFlags);
|
|
2004
|
+
const payloads = await buildCompanyUpdatePayloads(input, ctx, apiKey);
|
|
2005
|
+
const requestOpts = { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey };
|
|
2006
|
+
let basicResultCompany;
|
|
2007
|
+
if (needsBasic) {
|
|
2008
|
+
const { body } = await enterprisePatch(requestOpts, "/company/basic", payloads.basic, {
|
|
2009
|
+
idempotencyKey: basicKey
|
|
2010
|
+
});
|
|
2011
|
+
basicResultCompany = unwrapDataResponse(body);
|
|
2012
|
+
}
|
|
2013
|
+
if (!needsDescriptions) {
|
|
2014
|
+
printCompanyResult(basicResultCompany, ctx.format);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
const { body } = await enterprisePatch(
|
|
2019
|
+
requestOpts,
|
|
2020
|
+
"/company/descriptions",
|
|
2021
|
+
payloads.descriptions,
|
|
2022
|
+
{ idempotencyKey: descriptionsKey }
|
|
2023
|
+
);
|
|
2024
|
+
const result = unwrapDataResponse(body);
|
|
2025
|
+
printCompanyResult(result.company, ctx.format);
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
if (!needsBasic) {
|
|
2028
|
+
throw err;
|
|
2029
|
+
}
|
|
2030
|
+
const descriptionsBody = err instanceof ServerClientHttpError ? err.body : void 0;
|
|
2031
|
+
const partial = isDescriptionsPartialFailureBody(descriptionsBody) ? descriptionsBody : void 0;
|
|
2032
|
+
if (ctx.format === "json") {
|
|
2033
|
+
printJson({
|
|
2034
|
+
basic_updated: true,
|
|
2035
|
+
basic_company: basicResultCompany,
|
|
2036
|
+
descriptions_error: partial ? { updated_sections: partial.updated_sections, failed_section: partial.failed_section } : { message: err instanceof Error ? err.message : String(err) }
|
|
2037
|
+
});
|
|
2038
|
+
} else {
|
|
2039
|
+
process.stdout.write("Basic company info was updated successfully.\n");
|
|
2040
|
+
if (partial) {
|
|
2041
|
+
process.stdout.write(
|
|
2042
|
+
`Updated sections before failure: ${(partial.updated_sections ?? []).join(", ")}; failed section: ${partial.failed_section}
|
|
2043
|
+
`
|
|
2044
|
+
);
|
|
2045
|
+
} else {
|
|
2046
|
+
process.stdout.write(
|
|
2047
|
+
`Descriptions update failed entirely: ${err instanceof Error ? err.message : String(err)}
|
|
2048
|
+
`
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
throw err;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function registerEnterpriseCompanyUpdate(parent) {
|
|
2056
|
+
parent.command("update").description(
|
|
2057
|
+
"Update your company basic info and/or descriptions from a JSON file (uniform_number is read-only and cannot be changed via this command)"
|
|
2058
|
+
).requiredOption("--file <path>", 'path to a partial JSON company body, or "-" for stdin').option("--idempotency-key <key>", "single-section update only: reuse across retries (default: a fresh UUID)").option("--basic-idempotency-key <key>", "two-section update: idempotency key for PATCH /company/basic").option(
|
|
2059
|
+
"--descriptions-idempotency-key <key>",
|
|
2060
|
+
"two-section update: idempotency key for PATCH /company/descriptions"
|
|
2061
|
+
).action(async (flags, command) => {
|
|
2062
|
+
const ctx = resolveContext(command);
|
|
2063
|
+
const globals = command.optsWithGlobals();
|
|
2064
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2065
|
+
await runCompanyUpdate(
|
|
2066
|
+
ctx,
|
|
2067
|
+
key,
|
|
2068
|
+
flags.file,
|
|
2069
|
+
{
|
|
2070
|
+
idempotencyKey: flags.idempotencyKey,
|
|
2071
|
+
basicIdempotencyKey: flags.basicIdempotencyKey,
|
|
2072
|
+
descriptionsIdempotencyKey: flags.descriptionsIdempotencyKey
|
|
2073
|
+
},
|
|
2074
|
+
{ timeoutMs: ctx.timeoutMs }
|
|
2075
|
+
);
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// src/commands/enterprise/company/logo.ts
|
|
2080
|
+
var import_node_crypto6 = require("crypto");
|
|
2081
|
+
var import_node_fs7 = require("fs");
|
|
2082
|
+
var import_node_path3 = require("path");
|
|
2083
|
+
var EXTENSION_TO_CONTENT_TYPE = {
|
|
2084
|
+
".png": "image/png",
|
|
2085
|
+
".jpg": "image/jpeg",
|
|
2086
|
+
".jpeg": "image/jpeg"
|
|
2087
|
+
};
|
|
2088
|
+
var COMPANY_LOGO_MAX_SIZE = 2 * 1024 * 1024;
|
|
2089
|
+
function inspectLocalFile(path) {
|
|
2090
|
+
if (!(0, import_node_fs7.existsSync)(path)) {
|
|
2091
|
+
throw new InvalidArgumentError(`File not found: ${path}`);
|
|
2092
|
+
}
|
|
2093
|
+
const stat = (0, import_node_fs7.statSync)(path);
|
|
2094
|
+
if (!stat.isFile()) {
|
|
2095
|
+
throw new InvalidArgumentError(`Not a regular file: ${path}`);
|
|
2096
|
+
}
|
|
2097
|
+
const ext = (0, import_node_path3.extname)(path).toLowerCase();
|
|
2098
|
+
const contentType = EXTENSION_TO_CONTENT_TYPE[ext];
|
|
2099
|
+
if (!contentType) {
|
|
2100
|
+
throw new InvalidArgumentError(
|
|
2101
|
+
`Unsupported file extension "${ext || "(none)"}" \u2014 allowed: ${Object.keys(EXTENSION_TO_CONTENT_TYPE).join(", ")}`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
if (stat.size <= 0) {
|
|
2105
|
+
throw new InvalidArgumentError(`File is empty: ${path}`);
|
|
2106
|
+
}
|
|
2107
|
+
if (stat.size > COMPANY_LOGO_MAX_SIZE) {
|
|
2108
|
+
throw new InvalidArgumentError(`File too large: ${stat.size} bytes (max ${COMPANY_LOGO_MAX_SIZE} bytes / 2MB)`);
|
|
2109
|
+
}
|
|
2110
|
+
const bytes = (0, import_node_fs7.readFileSync)(path);
|
|
2111
|
+
return { contentType, fileSize: stat.size, bytes };
|
|
2112
|
+
}
|
|
2113
|
+
function resolveLogoIdempotencyKeys(flags) {
|
|
2114
|
+
if (flags.idempotencyKey) {
|
|
2115
|
+
return {
|
|
2116
|
+
presignKey: `${flags.idempotencyKey}-presign`,
|
|
2117
|
+
confirmKey: `${flags.idempotencyKey}-confirm`
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
return { presignKey: (0, import_node_crypto6.randomUUID)(), confirmKey: (0, import_node_crypto6.randomUUID)() };
|
|
2121
|
+
}
|
|
2122
|
+
function printLogoResult(result, format) {
|
|
2123
|
+
if (format === "json") {
|
|
2124
|
+
printJson(result);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
process.stdout.write(`Updated logo: ${result.logo_url}
|
|
2128
|
+
`);
|
|
2129
|
+
}
|
|
2130
|
+
async function runCompanyLogoUpload(ctx, apiKey, path, idempotencyFlags) {
|
|
2131
|
+
const { contentType, fileSize, bytes } = inspectLocalFile(path);
|
|
2132
|
+
const { presignKey, confirmKey } = resolveLogoIdempotencyKeys(idempotencyFlags);
|
|
2133
|
+
const requestOpts = { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey };
|
|
2134
|
+
const { body: presignBody } = await enterprisePost(
|
|
2135
|
+
requestOpts,
|
|
2136
|
+
"/company/logo/presign",
|
|
2137
|
+
{ content_type: contentType, file_size: fileSize },
|
|
2138
|
+
{ idempotencyKey: presignKey }
|
|
2139
|
+
);
|
|
2140
|
+
const presign = unwrapDataResponse(presignBody);
|
|
2141
|
+
const putRequest = new Request(presign.upload_url, {
|
|
2142
|
+
method: "PUT",
|
|
2143
|
+
headers: { "Content-Type": contentType },
|
|
2144
|
+
body: bytes
|
|
2145
|
+
});
|
|
2146
|
+
const putResponse = await fetchWithTimeout(putRequest, ctx.timeoutMs);
|
|
2147
|
+
if (!putResponse.ok) {
|
|
2148
|
+
throw new NetworkError(`Failed to upload file to S3: HTTP ${putResponse.status}`);
|
|
2149
|
+
}
|
|
2150
|
+
const { body: confirmBody } = await enterprisePost(
|
|
2151
|
+
requestOpts,
|
|
2152
|
+
"/company/logo/confirm",
|
|
2153
|
+
{ s3_key: presign.s3_key },
|
|
2154
|
+
{ idempotencyKey: confirmKey }
|
|
2155
|
+
);
|
|
2156
|
+
const result = unwrapDataResponse(confirmBody);
|
|
2157
|
+
printLogoResult(result, ctx.format);
|
|
2158
|
+
}
|
|
2159
|
+
function registerEnterpriseCompanyLogo(parent) {
|
|
2160
|
+
const logo = parent.command("logo").description("Manage your company logo");
|
|
2161
|
+
logo.command("upload <path>").description("Upload and set your company logo (presign + upload + confirm)").option("--idempotency-key <key>", "base key for presign/confirm (each derives its own; default: fresh UUIDs)").action(async (path, flags, command) => {
|
|
2162
|
+
const ctx = resolveContext(command);
|
|
2163
|
+
const globals = command.optsWithGlobals();
|
|
2164
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2165
|
+
await runCompanyLogoUpload(ctx, key, path, { idempotencyKey: flags.idempotencyKey });
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// src/commands/enterprise/company/index.ts
|
|
2170
|
+
function registerEnterpriseCompanyCommand(parent) {
|
|
2171
|
+
const company = parent.command("company").description("View and manage your company information");
|
|
2172
|
+
registerEnterpriseCompanyView(company);
|
|
2173
|
+
registerEnterpriseCompanyUpdate(company);
|
|
2174
|
+
registerEnterpriseCompanyLogo(company);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/commands/enterprise/talents/list.ts
|
|
2178
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
2179
|
+
var MINIMAL_LIST_FIELDS2 = ["enc_resume_id", "candidate_name", "applied_job_title", "applied_at"];
|
|
2180
|
+
function formatDate4(value) {
|
|
2181
|
+
return value ? String(value).slice(0, 10) : "";
|
|
2182
|
+
}
|
|
2183
|
+
async function runEnterpriseTalentsList(ctx, apiKey, flags) {
|
|
2184
|
+
if (flags.fields && flags.minimal) {
|
|
2185
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
2186
|
+
}
|
|
2187
|
+
const { body } = await enterpriseGet(
|
|
2188
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2189
|
+
"/talents",
|
|
2190
|
+
{
|
|
2191
|
+
tab: flags.tab,
|
|
2192
|
+
enc_job_id: flags.job,
|
|
2193
|
+
keyword: flags.keyword,
|
|
2194
|
+
start_date: flags.from,
|
|
2195
|
+
end_date: flags.to,
|
|
2196
|
+
currentPage: flags.page,
|
|
2197
|
+
pageSize: flags.pageSize ?? DEFAULT_PAGE_SIZE
|
|
2198
|
+
}
|
|
2199
|
+
);
|
|
2200
|
+
const paged = asPaginatedBody(body);
|
|
2201
|
+
const projection = flags.minimal ? MINIMAL_LIST_FIELDS2 : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
2202
|
+
if (projection || ctx.format === "json") {
|
|
2203
|
+
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
printTable(
|
|
2207
|
+
paged.data,
|
|
2208
|
+
[
|
|
2209
|
+
{ header: "RESUME_ID", value: (r) => (r.enc_resume_id ?? "").slice(0, 14) },
|
|
2210
|
+
{ header: "CANDIDATE", value: (r) => r.candidate_name ?? "", maxWidth: 20 },
|
|
2211
|
+
{ header: "EDUCATION", value: (r) => r.highest_education ?? "", maxWidth: 18 },
|
|
2212
|
+
{ header: "EXPERIENCE", value: (r) => r.latest_experience ?? "", maxWidth: 28 },
|
|
2213
|
+
{ header: "APPLIED_JOB", value: (r) => r.applied_job_title ?? "", maxWidth: 24 },
|
|
2214
|
+
{ header: "APPLIED_AT", value: (r) => formatDate4(r.applied_at), maxWidth: 12 },
|
|
2215
|
+
{ header: "VIEWED", value: (r) => r.is_viewed ? "yes" : "no" }
|
|
2216
|
+
],
|
|
2217
|
+
ctx.color
|
|
2218
|
+
);
|
|
2219
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} applicants).`;
|
|
2220
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise talents list --page ${paged.currentPage + 1}` : "";
|
|
2221
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
2222
|
+
}
|
|
2223
|
+
function registerEnterpriseTalentsList(parent) {
|
|
2224
|
+
parent.command("list").description("List applicants in your talent pool").option("--tab <tab>", "applied | visit (default applied; visit is not yet supported by the server)").option("--job <enc_job_id>", "filter by job posting enc_id").option("-k, --keyword <kw>", "filter by candidate name keyword").option("--from <date>", "applied on/after this date (YYYY-MM-DD)").option("--to <date>", "applied on/before this date (YYYY-MM-DD)").option("-p, --page <n>", "page number (server: currentPage, default 1)", (v) => Number(v)).option("-s, --page-size <n>", `items per page (default ${DEFAULT_PAGE_SIZE}, max 100)`, (v) => Number(v)).option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").option("--minimal", `output only ${MINIMAL_LIST_FIELDS2.join(",")} as JSON`).action(async (flags, command) => {
|
|
2225
|
+
const ctx = resolveContext(command);
|
|
2226
|
+
const globals = command.optsWithGlobals();
|
|
2227
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2228
|
+
await runEnterpriseTalentsList(ctx, key, flags);
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/commands/enterprise/talents/view.ts
|
|
2233
|
+
async function runEnterpriseTalentsView(ctx, apiKey, encResumeId, flags) {
|
|
2234
|
+
const trimmed = encResumeId.trim();
|
|
2235
|
+
if (!trimmed) {
|
|
2236
|
+
throw new CliError("enc_resume_id must not be empty", ExitCode.InvalidArgument);
|
|
2237
|
+
}
|
|
2238
|
+
const { body } = await enterpriseGet(
|
|
2239
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2240
|
+
`/talents/${encodeURIComponent(trimmed)}`
|
|
2241
|
+
);
|
|
2242
|
+
const resume = unwrapDataResponse(body);
|
|
2243
|
+
if (flags.fields) {
|
|
2244
|
+
printJson(pickPaths(resume, parseFieldsList(flags.fields)));
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
printJson(resume);
|
|
2248
|
+
}
|
|
2249
|
+
function registerEnterpriseTalentsView(parent) {
|
|
2250
|
+
parent.command("view <enc_resume_id>").description("View one applicant resume preview (company-scoped; PII visibility per access level)").option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").action(async (encResumeId, flags, command) => {
|
|
2251
|
+
const ctx = resolveContext(command);
|
|
2252
|
+
const globals = command.optsWithGlobals();
|
|
2253
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2254
|
+
await runEnterpriseTalentsView(ctx, key, encResumeId, flags);
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// src/commands/enterprise/talents/respond.ts
|
|
2259
|
+
var import_node_crypto7 = require("crypto");
|
|
2260
|
+
function resolveRespondBody(flags, options = {}) {
|
|
2261
|
+
const hasBody = flags.body !== void 0;
|
|
2262
|
+
const hasBodyFile = flags.bodyFile !== void 0;
|
|
2263
|
+
if (hasBody === hasBodyFile) {
|
|
2264
|
+
throw new CliError("Provide exactly one of --body or --body-file", ExitCode.InvalidArgument);
|
|
2265
|
+
}
|
|
2266
|
+
if (hasBody) {
|
|
2267
|
+
if (!flags.body.trim()) throw new CliError("--body must not be empty", ExitCode.InvalidArgument);
|
|
2268
|
+
return flags.body;
|
|
2269
|
+
}
|
|
2270
|
+
return readTextInput(flags.bodyFile, { timeoutMs: options.timeoutMs });
|
|
2271
|
+
}
|
|
2272
|
+
async function runEnterpriseTalentsRespond(ctx, apiKey, encResumeId, flags, idempotencyKey) {
|
|
2273
|
+
const trimmedId = encResumeId.trim();
|
|
2274
|
+
if (!trimmedId) {
|
|
2275
|
+
throw new CliError("enc_resume_id must not be empty", ExitCode.InvalidArgument);
|
|
2276
|
+
}
|
|
2277
|
+
const subject = (flags.subject ?? "").trim();
|
|
2278
|
+
if (!subject) {
|
|
2279
|
+
throw new CliError("--subject is required and must not be empty", ExitCode.InvalidArgument);
|
|
2280
|
+
}
|
|
2281
|
+
const body = resolveRespondBody(flags, { timeoutMs: ctx.timeoutMs });
|
|
2282
|
+
const encJobId = flags.encJobId?.trim();
|
|
2283
|
+
const payload = { subject, body };
|
|
2284
|
+
if (encJobId) payload.enc_job_id = encJobId;
|
|
2285
|
+
const { body: respBody } = await enterprisePost(
|
|
2286
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2287
|
+
`/talents/${encodeURIComponent(trimmedId)}/respond`,
|
|
2288
|
+
payload,
|
|
2289
|
+
{ idempotencyKey }
|
|
2290
|
+
);
|
|
2291
|
+
const result = unwrapDataResponse(respBody);
|
|
2292
|
+
if (ctx.format === "json") {
|
|
2293
|
+
printJson(result);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
process.stdout.write(`Replied to ${result.enc_resume_id ?? trimmedId}; sent at ${result.sent_at ?? "(unknown)"}.
|
|
2297
|
+
`);
|
|
2298
|
+
}
|
|
2299
|
+
function registerEnterpriseTalentsRespond(parent) {
|
|
2300
|
+
parent.command("respond <enc_resume_id>").description("Reply to an applicant (in-app message; inherits the company daily reply quota)").requiredOption("--subject <subject>", "message subject (max 200)").option("--body <text>", "message body text (max 5000)").option("--body-file <path>", 'read message body from a file, or "-" for stdin').option("--enc-job-id <enc_id>", "reply to a specific job posting; omit to reply to the most recent application").option("--idempotency-key <key>", "reuse across retries to avoid duplicate sends (default: a fresh UUID)").action(async (encResumeId, flags, command) => {
|
|
2301
|
+
const ctx = resolveContext(command);
|
|
2302
|
+
const globals = command.optsWithGlobals();
|
|
2303
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2304
|
+
await runEnterpriseTalentsRespond(ctx, key, encResumeId, flags, flags.idempotencyKey ?? (0, import_node_crypto7.randomUUID)());
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/commands/enterprise/talents/index.ts
|
|
2309
|
+
function registerEnterpriseTalentsCommand(parent) {
|
|
2310
|
+
const talents = parent.command("talents").description(
|
|
2311
|
+
"Browse & respond to your applied talent pool (visit tab n/a, no active candidate search \u2014 see `wport doctor`)"
|
|
2312
|
+
);
|
|
2313
|
+
registerEnterpriseTalentsList(talents);
|
|
2314
|
+
registerEnterpriseTalentsView(talents);
|
|
2315
|
+
registerEnterpriseTalentsRespond(talents);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// src/commands/enterprise/campaigns/create.ts
|
|
2319
|
+
var import_node_crypto8 = require("crypto");
|
|
2320
|
+
async function runCampaignCreate(ctx, apiKey, source, idempotencyKey) {
|
|
2321
|
+
const campaignBody = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
2322
|
+
const { body } = await enterprisePost(
|
|
2323
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2324
|
+
"/campaigns",
|
|
2325
|
+
campaignBody,
|
|
2326
|
+
{ idempotencyKey }
|
|
2327
|
+
);
|
|
2328
|
+
const created = unwrapDataResponse(body);
|
|
2329
|
+
if (ctx.format === "json") {
|
|
2330
|
+
printJson(created);
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
process.stdout.write(`Created campaign: ${created.enc_id ?? ""}
|
|
2334
|
+
`);
|
|
2335
|
+
}
|
|
2336
|
+
function registerEnterpriseCampaignsCreate(parent) {
|
|
2337
|
+
parent.command("create").description('Create a recruitment campaign from a JSON file (use "-" to read stdin)').requiredOption("--file <path>", 'path to a JSON campaign body, or "-" for stdin').option("--idempotency-key <key>", "reuse across retries to avoid duplicate creates (default: a fresh UUID)").action(async (flags, command) => {
|
|
2338
|
+
const ctx = resolveContext(command);
|
|
2339
|
+
const globals = command.optsWithGlobals();
|
|
2340
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2341
|
+
await runCampaignCreate(ctx, key, flags.file, flags.idempotencyKey ?? (0, import_node_crypto8.randomUUID)());
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// src/commands/enterprise/campaigns/list.ts
|
|
2346
|
+
var STATUS_MAP2 = { open: 1, closed: 0 };
|
|
2347
|
+
var MINIMAL_LIST_FIELDS3 = ["enc_id", "name", "status", "job_count"];
|
|
2348
|
+
function mapStatusFlag2(raw) {
|
|
2349
|
+
if (raw === void 0) return void 0;
|
|
2350
|
+
if (Object.prototype.hasOwnProperty.call(STATUS_MAP2, raw)) return STATUS_MAP2[raw];
|
|
2351
|
+
throw new CliError(`Invalid --status "${raw}". Allowed: ${Object.keys(STATUS_MAP2).join(", ")}`, ExitCode.InvalidArgument);
|
|
2352
|
+
}
|
|
2353
|
+
function formatStatus2(status) {
|
|
2354
|
+
if (status === 1) return "open";
|
|
2355
|
+
if (status === 0) return "closed";
|
|
2356
|
+
return status === void 0 ? "" : String(status);
|
|
2357
|
+
}
|
|
2358
|
+
function formatCount2(value) {
|
|
2359
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "\u2014";
|
|
2360
|
+
}
|
|
2361
|
+
async function runEnterpriseCampaignsList(ctx, apiKey, flags) {
|
|
2362
|
+
if (flags.fields && flags.minimal) {
|
|
2363
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
2364
|
+
}
|
|
2365
|
+
const { body } = await enterpriseGet(
|
|
2366
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2367
|
+
"/campaigns",
|
|
2368
|
+
{
|
|
2369
|
+
currentPage: flags.page,
|
|
2370
|
+
pageSize: flags.pageSize,
|
|
2371
|
+
keyword: flags.keyword,
|
|
2372
|
+
status: mapStatusFlag2(flags.status)
|
|
2373
|
+
}
|
|
2374
|
+
);
|
|
2375
|
+
const paged = asPaginatedBody(body);
|
|
2376
|
+
const projection = flags.minimal ? MINIMAL_LIST_FIELDS3 : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
2377
|
+
if (projection || ctx.format === "json") {
|
|
2378
|
+
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
printTable(
|
|
2382
|
+
paged.data,
|
|
2383
|
+
[
|
|
2384
|
+
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
2385
|
+
{ header: "NAME", value: (r) => r.name ?? "", maxWidth: 32 },
|
|
2386
|
+
{ header: "STATUS", value: (r) => formatStatus2(r.status) },
|
|
2387
|
+
{ header: "JOBS", value: (r) => formatCount2(r.job_count) },
|
|
2388
|
+
{ header: "PV", value: (r) => formatCount2(r.pv) },
|
|
2389
|
+
{ header: "VISITORS", value: (r) => formatCount2(r.visitors_count) }
|
|
2390
|
+
],
|
|
2391
|
+
ctx.color
|
|
2392
|
+
);
|
|
2393
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} campaigns).`;
|
|
2394
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise campaigns list --page ${paged.currentPage + 1}` : "";
|
|
2395
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
2396
|
+
}
|
|
2397
|
+
function registerEnterpriseCampaignsList(parent) {
|
|
2398
|
+
parent.command("list").description("List your recruitment campaigns").option("-p, --page <n>", "page number (server: currentPage, default 1)", (v) => Number(v)).option("-s, --page-size <n>", "items per page (default 10, max 100)", (v) => Number(v)).option("-k, --keyword <kw>", "filter by campaign name keyword").option("--status <state>", "filter by status: open | closed").option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").option("--minimal", `output only ${MINIMAL_LIST_FIELDS3.join(",")} as JSON`).action(async (flags, command) => {
|
|
2399
|
+
const ctx = resolveContext(command);
|
|
2400
|
+
const globals = command.optsWithGlobals();
|
|
2401
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2402
|
+
await runEnterpriseCampaignsList(ctx, key, flags);
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// src/commands/enterprise/campaigns/lifecycle.ts
|
|
2407
|
+
var import_node_crypto9 = require("crypto");
|
|
2408
|
+
async function runCampaignTransition(ctx, apiKey, encId, action, idempotencyKey) {
|
|
2409
|
+
const trimmed = requireEncId(encId);
|
|
2410
|
+
const { body } = await enterprisePatch(
|
|
2411
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2412
|
+
`/campaigns/${encodeURIComponent(trimmed)}/${action}`,
|
|
2413
|
+
{},
|
|
2414
|
+
{ idempotencyKey }
|
|
2415
|
+
);
|
|
2416
|
+
const result = unwrapDataResponse(body);
|
|
2417
|
+
if (ctx.format === "json") {
|
|
2418
|
+
printJson(result);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
const verb = action === "publish" ? "Published" : "Unpublished";
|
|
2422
|
+
process.stdout.write(`${verb} campaign: ${result.enc_id ?? trimmed}
|
|
2423
|
+
`);
|
|
2424
|
+
}
|
|
2425
|
+
function registerEnterpriseCampaignsPublish(parent) {
|
|
2426
|
+
parent.command("publish <enc_id>").description("Publish (activate) a recruitment campaign").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
2427
|
+
const ctx = resolveContext(command);
|
|
2428
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
2429
|
+
await runCampaignTransition(ctx, key, encId, "publish", flags.idempotencyKey ?? (0, import_node_crypto9.randomUUID)());
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
function registerEnterpriseCampaignsUnpublish(parent) {
|
|
2433
|
+
parent.command("unpublish <enc_id>").description("Unpublish (deactivate) a recruitment campaign").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
2434
|
+
const ctx = resolveContext(command);
|
|
2435
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
2436
|
+
await runCampaignTransition(ctx, key, encId, "unpublish", flags.idempotencyKey ?? (0, import_node_crypto9.randomUUID)());
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/commands/enterprise/campaigns/update.ts
|
|
2441
|
+
var import_node_crypto10 = require("crypto");
|
|
2442
|
+
async function runCampaignUpdate(ctx, apiKey, encId, source, idempotencyKey) {
|
|
2443
|
+
const trimmed = requireEncId(encId);
|
|
2444
|
+
const campaignBody = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
2445
|
+
const { body } = await enterprisePatch(
|
|
2446
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2447
|
+
`/campaigns/${encodeURIComponent(trimmed)}`,
|
|
2448
|
+
campaignBody,
|
|
2449
|
+
{ idempotencyKey }
|
|
2450
|
+
);
|
|
2451
|
+
const updated = unwrapDataResponse(body);
|
|
2452
|
+
if (ctx.format === "json") {
|
|
2453
|
+
printJson(updated);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
process.stdout.write(`Updated campaign: ${updated.enc_id ?? trimmed}
|
|
2457
|
+
`);
|
|
2458
|
+
}
|
|
2459
|
+
function registerEnterpriseCampaignsUpdate(parent) {
|
|
2460
|
+
parent.command("update <enc_id>").description('Update a recruitment campaign from a JSON file (use "-" to read stdin)').requiredOption("--file <path>", 'path to a partial JSON campaign body, or "-" for stdin').option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
2461
|
+
const ctx = resolveContext(command);
|
|
2462
|
+
const globals = command.optsWithGlobals();
|
|
2463
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2464
|
+
await runCampaignUpdate(ctx, key, encId, flags.file, flags.idempotencyKey ?? (0, import_node_crypto10.randomUUID)());
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// src/commands/enterprise/campaigns/view.ts
|
|
2469
|
+
async function runEnterpriseCampaignsView(ctx, apiKey, encId, flags) {
|
|
2470
|
+
const trimmed = encId.trim();
|
|
2471
|
+
if (!trimmed) {
|
|
2472
|
+
throw new CliError("enc_id must not be empty", ExitCode.InvalidArgument);
|
|
2473
|
+
}
|
|
2474
|
+
const { body } = await enterpriseGet(
|
|
2475
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2476
|
+
`/campaigns/${encodeURIComponent(trimmed)}`
|
|
2477
|
+
);
|
|
2478
|
+
const campaign = unwrapDataResponse(body);
|
|
2479
|
+
if (flags.fields) {
|
|
2480
|
+
printJson(pickPaths(campaign, parseFieldsList(flags.fields)));
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
printJson(campaign);
|
|
2484
|
+
}
|
|
2485
|
+
function registerEnterpriseCampaignsView(parent) {
|
|
2486
|
+
parent.command("view <enc_id>").description("View one recruitment campaign (with its jobs)").option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").action(async (encId, flags, command) => {
|
|
2487
|
+
const ctx = resolveContext(command);
|
|
2488
|
+
const globals = command.optsWithGlobals();
|
|
2489
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2490
|
+
await runEnterpriseCampaignsView(ctx, key, encId, flags);
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/commands/enterprise/campaigns/index.ts
|
|
2495
|
+
function registerEnterpriseCampaignsCommand(parent) {
|
|
2496
|
+
const campaigns = parent.command("campaigns").description("Manage your recruitment campaigns");
|
|
2497
|
+
registerEnterpriseCampaignsList(campaigns);
|
|
2498
|
+
registerEnterpriseCampaignsView(campaigns);
|
|
2499
|
+
registerEnterpriseCampaignsCreate(campaigns);
|
|
2500
|
+
registerEnterpriseCampaignsUpdate(campaigns);
|
|
2501
|
+
registerEnterpriseCampaignsPublish(campaigns);
|
|
2502
|
+
registerEnterpriseCampaignsUnpublish(campaigns);
|
|
2503
|
+
}
|
|
2504
|
+
|
|
1639
2505
|
// src/commands/enterprise/index.ts
|
|
1640
2506
|
function registerEnterpriseCommand(program2) {
|
|
1641
2507
|
const enterprise = program2.command("enterprise").description("Manage your company job postings with an enterprise API key").option("--api-key <key>", 'one-off API key (prefer "wport enterprise login" or the WPORT_API_KEY env var)');
|
|
1642
2508
|
registerEnterpriseLogin(enterprise);
|
|
1643
2509
|
registerEnterpriseLogout(enterprise);
|
|
1644
2510
|
registerEnterpriseWhoami(enterprise);
|
|
2511
|
+
registerEnterpriseUsage(enterprise);
|
|
1645
2512
|
registerEnterpriseJobsCommand(enterprise);
|
|
1646
2513
|
registerEnterpriseKeysCommand(enterprise);
|
|
2514
|
+
registerEnterpriseCompanyCommand(enterprise);
|
|
2515
|
+
registerEnterpriseTalentsCommand(enterprise);
|
|
2516
|
+
registerEnterpriseCampaignsCommand(enterprise);
|
|
1647
2517
|
}
|
|
1648
2518
|
|
|
1649
2519
|
// src/index.ts
|
|
1650
2520
|
var program = new import_commander.Command();
|
|
1651
|
-
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.
|
|
2521
|
+
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.6.0", "-v, --version", "output the CLI version").option("--lang <locale>", "Accept-Language locale: zh-TW | en-US | vi-VN | th-TH | id-ID").option("--api <url>", "override API base URL").option("--output <fmt>", "output format: table | json").option("--no-color", "disable color output").option("--timeout <ms>", "HTTP timeout in milliseconds", (v) => Number(v));
|
|
1652
2522
|
registerJobsCommand(program);
|
|
1653
2523
|
registerConfigCommand(program);
|
|
1654
2524
|
registerDoctorCommand(program);
|