@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/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.4.0"} (node ${process.version}; ${process.platform})`;
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.4.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
- const { body } = await enterpriseGet(
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.4.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));
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);