@wport/cli 0.2.2 → 0.5.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 +31 -4
- package/README.md +28 -0
- package/dist/index.js +1219 -41
- 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.5.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) {
|
|
@@ -187,6 +187,13 @@ function unwrapDataResponse(body) {
|
|
|
187
187
|
}
|
|
188
188
|
throw new CliError("Unexpected response shape: missing { success, data } wrapper", ExitCode.ServerOrNetworkError);
|
|
189
189
|
}
|
|
190
|
+
function unwrapDataArray(body) {
|
|
191
|
+
const data = unwrapDataResponse(body);
|
|
192
|
+
if (!Array.isArray(data)) {
|
|
193
|
+
throw new CliError("Unexpected response shape: `data` is not an array", ExitCode.ServerOrNetworkError);
|
|
194
|
+
}
|
|
195
|
+
return data;
|
|
196
|
+
}
|
|
190
197
|
function asPaginatedBody(body) {
|
|
191
198
|
if (!body || typeof body !== "object") {
|
|
192
199
|
throw new CliError("Unexpected response shape: not an object", ExitCode.ServerOrNetworkError);
|
|
@@ -633,6 +640,57 @@ function promptSecret(promptText, options = {}) {
|
|
|
633
640
|
stdin.on("data", onData);
|
|
634
641
|
});
|
|
635
642
|
}
|
|
643
|
+
function readJsonInput(source, options = {}) {
|
|
644
|
+
const readStdin = options.readStdin ?? ((label) => readPipedStdin(label, { timeoutMs: options.timeoutMs }));
|
|
645
|
+
let raw;
|
|
646
|
+
if (source === "-") {
|
|
647
|
+
raw = readStdin("--file -");
|
|
648
|
+
} else {
|
|
649
|
+
try {
|
|
650
|
+
raw = (0, import_node_fs3.readFileSync)(source, "utf8");
|
|
651
|
+
} catch (err) {
|
|
652
|
+
throw new InvalidArgumentError(
|
|
653
|
+
`Cannot read --file "${source}": ${err.code ?? err.message}`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (!raw.trim()) {
|
|
658
|
+
throw new InvalidArgumentError("Input is empty \u2014 expected a JSON body");
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
return JSON.parse(raw);
|
|
662
|
+
} catch {
|
|
663
|
+
throw new InvalidArgumentError("Invalid JSON in input (parse failed)");
|
|
664
|
+
}
|
|
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
|
+
}
|
|
685
|
+
function readJsonObject(source, options = {}) {
|
|
686
|
+
const parsed = readJsonInput(source, options);
|
|
687
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
688
|
+
throw new InvalidArgumentError(
|
|
689
|
+
`Input must be a JSON object, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
return parsed;
|
|
693
|
+
}
|
|
636
694
|
|
|
637
695
|
// src/commands/jobs/view.ts
|
|
638
696
|
var import_picocolors2 = __toESM(require("picocolors"));
|
|
@@ -920,7 +978,7 @@ function registerDoctorCommand(program2) {
|
|
|
920
978
|
).action(async (_opts, command) => {
|
|
921
979
|
const ctx = resolveContext(command);
|
|
922
980
|
const line = (s = "") => process.stdout.write(s + "\n");
|
|
923
|
-
line(`wport-cli ${"0.
|
|
981
|
+
line(`wport-cli ${"0.5.0"}`);
|
|
924
982
|
line(` bundled schema fingerprint: ${"839e8a891dfb"}`);
|
|
925
983
|
line("");
|
|
926
984
|
line("Resolved configuration:");
|
|
@@ -984,19 +1042,48 @@ async function enterpriseGet(opts, path, query) {
|
|
|
984
1042
|
warnIfRateLimitLow(res.headers);
|
|
985
1043
|
return { body, headers: res.headers };
|
|
986
1044
|
}
|
|
1045
|
+
async function enterpriseWrite(method, opts, path, body, extra) {
|
|
1046
|
+
const url = new URL(`${opts.baseUrl}${ENTERPRISE_PREFIX}${path}`);
|
|
1047
|
+
const headers = {
|
|
1048
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
1049
|
+
"Accept-Language": opts.locale,
|
|
1050
|
+
"User-Agent": buildUserAgent(),
|
|
1051
|
+
Accept: "application/json"
|
|
1052
|
+
};
|
|
1053
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
1054
|
+
if (extra?.idempotencyKey) headers["Idempotency-Key"] = extra.idempotencyKey;
|
|
1055
|
+
if (extra?.ifMatch) headers["If-Match"] = extra.ifMatch;
|
|
1056
|
+
const request = new Request(url, {
|
|
1057
|
+
method,
|
|
1058
|
+
headers,
|
|
1059
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1060
|
+
});
|
|
1061
|
+
const res = await fetchWithTimeout(request, opts.timeoutMs);
|
|
1062
|
+
const respBody = await res.json().catch(() => null);
|
|
1063
|
+
if (!res.ok) throwEnterpriseHttpError(res.status, respBody);
|
|
1064
|
+
warnIfRateLimitLow(res.headers);
|
|
1065
|
+
return { body: respBody, headers: res.headers };
|
|
1066
|
+
}
|
|
1067
|
+
function enterprisePost(opts, path, body, extra) {
|
|
1068
|
+
return enterpriseWrite("POST", opts, path, body, extra);
|
|
1069
|
+
}
|
|
1070
|
+
function enterprisePatch(opts, path, body, extra) {
|
|
1071
|
+
return enterpriseWrite("PATCH", opts, path, body, extra);
|
|
1072
|
+
}
|
|
1073
|
+
function enterpriseDelete(opts, path, extra) {
|
|
1074
|
+
return enterpriseWrite("DELETE", opts, path, void 0, extra);
|
|
1075
|
+
}
|
|
987
1076
|
function throwEnterpriseHttpError(status, body) {
|
|
988
1077
|
const base = extractErrorMessage(body) ?? `HTTP ${status}`;
|
|
989
1078
|
if (status === 401) {
|
|
990
1079
|
throw new CliError(
|
|
991
|
-
`${base}
|
|
992
|
-
Key may have been revoked \u2014 run \`wport enterprise login\` to re-authenticate.`,
|
|
1080
|
+
`${base} \u2014 If your key has expired, rotate it in place: \`wport enterprise keys rotate <enc_id>\` (an expired key is still accepted for rotate). If it was revoked or is incorrect, obtain a valid key and run \`wport enterprise login\`.`,
|
|
993
1081
|
ExitCode.ServerClientError
|
|
994
1082
|
);
|
|
995
1083
|
}
|
|
996
1084
|
if (status === 403) {
|
|
997
1085
|
throw new CliError(
|
|
998
|
-
`${base}
|
|
999
|
-
If your company account has been suspended, please contact support.`,
|
|
1086
|
+
`${base} \u2014 Your key may lack the required scope; rotate or issue a key that includes it. If your company account has been suspended, please contact support.`,
|
|
1000
1087
|
ExitCode.ServerClientError
|
|
1001
1088
|
);
|
|
1002
1089
|
}
|
|
@@ -1183,6 +1270,41 @@ function registerEnterpriseWhoami(parent) {
|
|
|
1183
1270
|
});
|
|
1184
1271
|
}
|
|
1185
1272
|
|
|
1273
|
+
// src/commands/enterprise/usage.ts
|
|
1274
|
+
function num(value) {
|
|
1275
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "\u2014";
|
|
1276
|
+
}
|
|
1277
|
+
async function runUsage(ctx, apiKey) {
|
|
1278
|
+
const { body } = await enterpriseGet(
|
|
1279
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1280
|
+
"/usage"
|
|
1281
|
+
);
|
|
1282
|
+
const usage = unwrapDataResponse(body);
|
|
1283
|
+
if (ctx.format === "json") {
|
|
1284
|
+
printJson(usage);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const quota = usage.quota ?? {};
|
|
1288
|
+
const rate = usage.rate_limit ?? {};
|
|
1289
|
+
const lines = [
|
|
1290
|
+
`period: ${usage.period ?? "\u2014"}`,
|
|
1291
|
+
`monthly quota: ${num(quota.used)} / ${num(quota.limit)} used (${num(quota.remaining)} remaining)`,
|
|
1292
|
+
`rate limit: ${num(rate.limit)} requests / ${num(rate.window_seconds)}s per key`
|
|
1293
|
+
];
|
|
1294
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1295
|
+
process.stdout.write(
|
|
1296
|
+
dim("Live per-window rate-limit headroom is reported via response headers on write requests.", ctx.color) + "\n"
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
function registerEnterpriseUsage(parent) {
|
|
1300
|
+
parent.command("usage").description("Show this month API quota usage and rate-limit ceiling").action(async (_flags, command) => {
|
|
1301
|
+
const ctx = resolveContext(command);
|
|
1302
|
+
const globals = command.optsWithGlobals();
|
|
1303
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1304
|
+
await runUsage(ctx, key);
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1186
1308
|
// src/commands/enterprise/jobs/list.ts
|
|
1187
1309
|
var STATUS_MAP = { published: 1, unpublished: 0 };
|
|
1188
1310
|
var MINIMAL_LIST_FIELDS = ["enc_id", "job_title", "status", "updated_at"];
|
|
@@ -1202,44 +1324,51 @@ function formatStatus(status) {
|
|
|
1202
1324
|
function formatDate2(value) {
|
|
1203
1325
|
return value ? String(value).slice(0, 10) : "";
|
|
1204
1326
|
}
|
|
1327
|
+
function formatCount(value) {
|
|
1328
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "\u2014";
|
|
1329
|
+
}
|
|
1330
|
+
async function runEnterpriseJobsList(ctx, apiKey, flags) {
|
|
1331
|
+
if (flags.fields && flags.minimal) {
|
|
1332
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
1333
|
+
}
|
|
1334
|
+
const { body } = await enterpriseGet(
|
|
1335
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1336
|
+
"/jobs",
|
|
1337
|
+
{
|
|
1338
|
+
currentPage: flags.page,
|
|
1339
|
+
pageSize: flags.pageSize,
|
|
1340
|
+
keyword: flags.keyword,
|
|
1341
|
+
status: mapStatusFlag(flags.status)
|
|
1342
|
+
}
|
|
1343
|
+
);
|
|
1344
|
+
const paged = asPaginatedBody(body);
|
|
1345
|
+
const projection = flags.minimal ? MINIMAL_LIST_FIELDS : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
1346
|
+
if (projection || ctx.format === "json") {
|
|
1347
|
+
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
printTable(
|
|
1351
|
+
paged.data,
|
|
1352
|
+
[
|
|
1353
|
+
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
1354
|
+
{ header: "TITLE", value: (r) => r.job_title ?? "", maxWidth: 36 },
|
|
1355
|
+
{ header: "STATUS", value: (r) => formatStatus(r.status) },
|
|
1356
|
+
{ header: "CLICKS_7D", value: (r) => formatCount(r.clicks_7d) },
|
|
1357
|
+
{ header: "PUBLISHED", value: (r) => formatDate2(r.published_at), maxWidth: 12 },
|
|
1358
|
+
{ header: "UPDATED", value: (r) => formatDate2(r.updated_at), maxWidth: 12 }
|
|
1359
|
+
],
|
|
1360
|
+
ctx.color
|
|
1361
|
+
);
|
|
1362
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} jobs).`;
|
|
1363
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise jobs list --page ${paged.currentPage + 1}` : "";
|
|
1364
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
1365
|
+
}
|
|
1205
1366
|
function registerEnterpriseJobsList(parent) {
|
|
1206
1367
|
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) => {
|
|
1207
1368
|
const ctx = resolveContext(command);
|
|
1208
|
-
if (flags.fields && flags.minimal) {
|
|
1209
|
-
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
1210
|
-
}
|
|
1211
1369
|
const globals = command.optsWithGlobals();
|
|
1212
1370
|
const { key } = resolveApiKey(globals.apiKey);
|
|
1213
|
-
|
|
1214
|
-
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey: key },
|
|
1215
|
-
"/jobs",
|
|
1216
|
-
{
|
|
1217
|
-
currentPage: flags.page,
|
|
1218
|
-
pageSize: flags.pageSize,
|
|
1219
|
-
keyword: flags.keyword,
|
|
1220
|
-
status: mapStatusFlag(flags.status)
|
|
1221
|
-
}
|
|
1222
|
-
);
|
|
1223
|
-
const paged = asPaginatedBody(body);
|
|
1224
|
-
const projection = flags.minimal ? MINIMAL_LIST_FIELDS : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
1225
|
-
if (projection || ctx.format === "json") {
|
|
1226
|
-
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
printTable(
|
|
1230
|
-
paged.data,
|
|
1231
|
-
[
|
|
1232
|
-
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
1233
|
-
{ header: "TITLE", value: (r) => r.job_title ?? "", maxWidth: 36 },
|
|
1234
|
-
{ header: "STATUS", value: (r) => formatStatus(r.status) },
|
|
1235
|
-
{ header: "CODE", value: (r) => r.code ?? "", maxWidth: 16 },
|
|
1236
|
-
{ header: "UPDATED", value: (r) => formatDate2(r.updated_at), maxWidth: 12 }
|
|
1237
|
-
],
|
|
1238
|
-
ctx.color
|
|
1239
|
-
);
|
|
1240
|
-
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} jobs).`;
|
|
1241
|
-
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise jobs list --page ${paged.currentPage + 1}` : "";
|
|
1242
|
-
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
1371
|
+
await runEnterpriseJobsList(ctx, key, flags);
|
|
1243
1372
|
});
|
|
1244
1373
|
}
|
|
1245
1374
|
|
|
@@ -1281,11 +1410,1055 @@ function registerEnterpriseJobsView(parent) {
|
|
|
1281
1410
|
});
|
|
1282
1411
|
}
|
|
1283
1412
|
|
|
1413
|
+
// src/commands/enterprise/jobs/create.ts
|
|
1414
|
+
var import_node_crypto = require("crypto");
|
|
1415
|
+
async function runJobsCreate(ctx, apiKey, source, idempotencyKey) {
|
|
1416
|
+
const jobBody = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
1417
|
+
const { body } = await enterprisePost(
|
|
1418
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1419
|
+
"/jobs",
|
|
1420
|
+
jobBody,
|
|
1421
|
+
{ idempotencyKey }
|
|
1422
|
+
);
|
|
1423
|
+
const created = unwrapDataResponse(body);
|
|
1424
|
+
if (ctx.format === "json") {
|
|
1425
|
+
printJson(created);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
process.stdout.write(`Created job: ${created.enc_id ?? ""}
|
|
1429
|
+
`);
|
|
1430
|
+
}
|
|
1431
|
+
function registerEnterpriseJobsCreate(parent) {
|
|
1432
|
+
parent.command("create").description('Create a job posting from a JSON file (use "-" to read stdin)').requiredOption("--file <path>", 'path to a JSON job body, or "-" for stdin').option("--idempotency-key <key>", "reuse across retries to avoid duplicate creates (default: a fresh UUID)").action(async (flags, command) => {
|
|
1433
|
+
const ctx = resolveContext(command);
|
|
1434
|
+
const globals = command.optsWithGlobals();
|
|
1435
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1436
|
+
await runJobsCreate(ctx, key, flags.file, flags.idempotencyKey ?? (0, import_node_crypto.randomUUID)());
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/commands/enterprise/jobs/update.ts
|
|
1441
|
+
var import_node_crypto2 = require("crypto");
|
|
1442
|
+
|
|
1443
|
+
// src/commands/enterprise/jobs/write-shared.ts
|
|
1444
|
+
function requireEncId(encId) {
|
|
1445
|
+
const trimmed = encId.trim();
|
|
1446
|
+
if (!trimmed) throw new CliError("enc_id must not be empty", ExitCode.InvalidArgument);
|
|
1447
|
+
return trimmed;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/commands/enterprise/jobs/update.ts
|
|
1451
|
+
async function runJobsUpdate(ctx, apiKey, encId, source, idempotencyKey, ifMatch) {
|
|
1452
|
+
const trimmed = requireEncId(encId);
|
|
1453
|
+
const jobBody = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
1454
|
+
const { body } = await enterprisePatch(
|
|
1455
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1456
|
+
`/jobs/${encodeURIComponent(trimmed)}`,
|
|
1457
|
+
jobBody,
|
|
1458
|
+
{ idempotencyKey, ifMatch }
|
|
1459
|
+
);
|
|
1460
|
+
const updated = unwrapDataResponse(body);
|
|
1461
|
+
if (ctx.format === "json") {
|
|
1462
|
+
printJson(updated);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
process.stdout.write(`Updated job: ${updated.enc_id ?? trimmed}
|
|
1466
|
+
`);
|
|
1467
|
+
}
|
|
1468
|
+
function registerEnterpriseJobsUpdate(parent) {
|
|
1469
|
+
parent.command("update <enc_id>").description('Update a job posting from a JSON file (partial; use "-" for stdin)').requiredOption("--file <path>", 'path to a partial JSON job body, or "-" for stdin').option("--if-match <updated_at>", "optimistic lock: the job's current updated_at (409 if stale)").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
1470
|
+
const ctx = resolveContext(command);
|
|
1471
|
+
const globals = command.optsWithGlobals();
|
|
1472
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1473
|
+
await runJobsUpdate(ctx, key, encId, flags.file, flags.idempotencyKey ?? (0, import_node_crypto2.randomUUID)(), flags.ifMatch);
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/commands/enterprise/jobs/lifecycle.ts
|
|
1478
|
+
var import_node_crypto3 = require("crypto");
|
|
1479
|
+
async function runJobsTransition(ctx, apiKey, encId, action, idempotencyKey) {
|
|
1480
|
+
const trimmed = requireEncId(encId);
|
|
1481
|
+
const { body } = await enterprisePatch(
|
|
1482
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1483
|
+
`/jobs/${encodeURIComponent(trimmed)}/${action}`,
|
|
1484
|
+
{},
|
|
1485
|
+
{ idempotencyKey }
|
|
1486
|
+
);
|
|
1487
|
+
const result = unwrapDataResponse(body);
|
|
1488
|
+
if (ctx.format === "json") {
|
|
1489
|
+
printJson(result);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const verb = action === "publish" ? "Published" : "Unpublished";
|
|
1493
|
+
process.stdout.write(`${verb} job: ${result.enc_id ?? trimmed}
|
|
1494
|
+
`);
|
|
1495
|
+
}
|
|
1496
|
+
async function runJobsDelete(ctx, apiKey, encId, confirm, idempotencyKey) {
|
|
1497
|
+
if (!confirm) {
|
|
1498
|
+
throw new CliError("Refusing to delete without --confirm (destructive, irreversible)", ExitCode.InvalidArgument);
|
|
1499
|
+
}
|
|
1500
|
+
const trimmed = requireEncId(encId);
|
|
1501
|
+
await enterpriseDelete(
|
|
1502
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1503
|
+
`/jobs/${encodeURIComponent(trimmed)}`,
|
|
1504
|
+
{ idempotencyKey }
|
|
1505
|
+
);
|
|
1506
|
+
if (ctx.format === "json") {
|
|
1507
|
+
printJson({ enc_id: trimmed, deleted: true });
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
process.stdout.write(`Deleted job: ${trimmed}
|
|
1511
|
+
`);
|
|
1512
|
+
}
|
|
1513
|
+
async function runJobsCopy(ctx, apiKey, encId, idempotencyKey) {
|
|
1514
|
+
const trimmed = requireEncId(encId);
|
|
1515
|
+
const { body } = await enterprisePost(
|
|
1516
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1517
|
+
`/jobs/${encodeURIComponent(trimmed)}/copy`,
|
|
1518
|
+
{},
|
|
1519
|
+
{ idempotencyKey }
|
|
1520
|
+
);
|
|
1521
|
+
const result = unwrapDataResponse(body);
|
|
1522
|
+
if (ctx.format === "json") {
|
|
1523
|
+
printJson(result);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
process.stdout.write(`Copied job ${trimmed} \u2192 new draft: ${result.enc_id ?? "(unknown)"}
|
|
1527
|
+
`);
|
|
1528
|
+
}
|
|
1529
|
+
function registerEnterpriseJobsCopy(parent) {
|
|
1530
|
+
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) => {
|
|
1531
|
+
const ctx = resolveContext(command);
|
|
1532
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1533
|
+
await runJobsCopy(ctx, key, encId, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
async function runJobsClose(ctx, apiKey, encId, confirm, idempotencyKey) {
|
|
1537
|
+
if (!confirm) {
|
|
1538
|
+
throw new CliError(
|
|
1539
|
+
"Refusing to close without --confirm (irreversible; a closed job cannot be reopened \u2014 copy it to relist)",
|
|
1540
|
+
ExitCode.InvalidArgument
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const trimmed = requireEncId(encId);
|
|
1544
|
+
const { body } = await enterprisePatch(
|
|
1545
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1546
|
+
`/jobs/${encodeURIComponent(trimmed)}/close`,
|
|
1547
|
+
{},
|
|
1548
|
+
{ idempotencyKey }
|
|
1549
|
+
);
|
|
1550
|
+
const result = unwrapDataResponse(body);
|
|
1551
|
+
if (ctx.format === "json") {
|
|
1552
|
+
printJson(result);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
process.stdout.write(`Closed job: ${result.enc_id ?? trimmed}
|
|
1556
|
+
`);
|
|
1557
|
+
}
|
|
1558
|
+
function registerEnterpriseJobsClose(parent) {
|
|
1559
|
+
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) => {
|
|
1560
|
+
const ctx = resolveContext(command);
|
|
1561
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1562
|
+
await runJobsClose(ctx, key, encId, flags.confirm === true, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
function registerEnterpriseJobsPublish(parent) {
|
|
1566
|
+
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) => {
|
|
1567
|
+
const ctx = resolveContext(command);
|
|
1568
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1569
|
+
await runJobsTransition(ctx, key, encId, "publish", flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
function registerEnterpriseJobsUnpublish(parent) {
|
|
1573
|
+
parent.command("unpublish <enc_id>").description("Unpublish (take down) a job posting").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
1574
|
+
const ctx = resolveContext(command);
|
|
1575
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1576
|
+
await runJobsTransition(ctx, key, encId, "unpublish", flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
function registerEnterpriseJobsDelete(parent) {
|
|
1580
|
+
parent.command("delete <enc_id>").description("Delete a job posting (destructive; requires --confirm)").option("--confirm", "confirm this destructive, irreversible delete").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (encId, flags, command) => {
|
|
1581
|
+
const ctx = resolveContext(command);
|
|
1582
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1583
|
+
await runJobsDelete(ctx, key, encId, flags.confirm === true, flags.idempotencyKey ?? (0, import_node_crypto3.randomUUID)());
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/commands/enterprise/jobs/batch.ts
|
|
1588
|
+
var import_node_crypto4 = require("crypto");
|
|
1589
|
+
var BATCH_MIN = 1;
|
|
1590
|
+
var BATCH_MAX = 10;
|
|
1591
|
+
async function runJobsBatch(ctx, apiKey, source, confirm, idempotencyKey) {
|
|
1592
|
+
if (!confirm) {
|
|
1593
|
+
throw new CliError("Refusing to run batch create without --confirm", ExitCode.InvalidArgument);
|
|
1594
|
+
}
|
|
1595
|
+
const payload = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
1596
|
+
const jobs = payload.jobs;
|
|
1597
|
+
if (!Array.isArray(jobs) || jobs.length < BATCH_MIN || jobs.length > BATCH_MAX) {
|
|
1598
|
+
throw new CliError(
|
|
1599
|
+
`Batch input must be { "jobs": [...] } with ${BATCH_MIN} to ${BATCH_MAX} items`,
|
|
1600
|
+
ExitCode.InvalidArgument
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const { body } = await enterprisePost(
|
|
1604
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1605
|
+
"/jobs/batch",
|
|
1606
|
+
payload,
|
|
1607
|
+
{ idempotencyKey }
|
|
1608
|
+
);
|
|
1609
|
+
const result = unwrapDataResponse(body);
|
|
1610
|
+
const succeeded = result.succeeded ?? [];
|
|
1611
|
+
const failed = result.failed ?? [];
|
|
1612
|
+
if (ctx.format === "json") {
|
|
1613
|
+
printJson(result);
|
|
1614
|
+
} else {
|
|
1615
|
+
process.stdout.write(`Batch: ${succeeded.length} succeeded, ${failed.length} failed (of ${jobs.length}).
|
|
1616
|
+
`);
|
|
1617
|
+
for (const s of succeeded)
|
|
1618
|
+
process.stdout.write(` ok [${s.index}] ${sanitizeForTerminal(String(s.enc_id ?? ""))}
|
|
1619
|
+
`);
|
|
1620
|
+
for (const f of failed)
|
|
1621
|
+
process.stdout.write(` fail [${f.index}] ${sanitizeForTerminal(String(f.error_path ?? ""))}
|
|
1622
|
+
`);
|
|
1623
|
+
}
|
|
1624
|
+
if (failed.length > 0) {
|
|
1625
|
+
throw new CliError(`${failed.length} of ${jobs.length} job(s) failed in batch create`, ExitCode.ServerClientError);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function registerEnterpriseJobsBatch(parent) {
|
|
1629
|
+
parent.command("batch").description('Batch-create up to 10 jobs from a JSON file ({ "jobs": [...] }; requires --confirm)').requiredOption("--file <path>", 'path to a JSON { "jobs": [...] } body, or "-" for stdin').option("--confirm", "confirm this bulk write").option("--idempotency-key <key>", "reuse across retries (default: a fresh UUID)").action(async (flags, command) => {
|
|
1630
|
+
const ctx = resolveContext(command);
|
|
1631
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
1632
|
+
await runJobsBatch(ctx, key, flags.file, flags.confirm === true, flags.idempotencyKey ?? (0, import_node_crypto4.randomUUID)());
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1284
1636
|
// src/commands/enterprise/jobs/index.ts
|
|
1285
1637
|
function registerEnterpriseJobsCommand(parent) {
|
|
1286
|
-
const jobs = parent.command("jobs").description("
|
|
1638
|
+
const jobs = parent.command("jobs").description("Manage your company job postings");
|
|
1287
1639
|
registerEnterpriseJobsList(jobs);
|
|
1288
1640
|
registerEnterpriseJobsView(jobs);
|
|
1641
|
+
registerEnterpriseJobsCreate(jobs);
|
|
1642
|
+
registerEnterpriseJobsUpdate(jobs);
|
|
1643
|
+
registerEnterpriseJobsPublish(jobs);
|
|
1644
|
+
registerEnterpriseJobsUnpublish(jobs);
|
|
1645
|
+
registerEnterpriseJobsDelete(jobs);
|
|
1646
|
+
registerEnterpriseJobsCopy(jobs);
|
|
1647
|
+
registerEnterpriseJobsClose(jobs);
|
|
1648
|
+
registerEnterpriseJobsBatch(jobs);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/commands/enterprise/keys/list.ts
|
|
1652
|
+
function formatDate3(value) {
|
|
1653
|
+
return value ? String(value).slice(0, 10) : "";
|
|
1654
|
+
}
|
|
1655
|
+
function formatScopes(scopes) {
|
|
1656
|
+
return Array.isArray(scopes) ? scopes.join(",") : "";
|
|
1657
|
+
}
|
|
1658
|
+
async function runKeysList(ctx, apiKey) {
|
|
1659
|
+
const { body } = await enterpriseGet(
|
|
1660
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1661
|
+
"/keys"
|
|
1662
|
+
);
|
|
1663
|
+
const keys = unwrapDataArray(body);
|
|
1664
|
+
if (ctx.format === "json") {
|
|
1665
|
+
printJson(keys);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
printTable(
|
|
1669
|
+
keys,
|
|
1670
|
+
[
|
|
1671
|
+
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
1672
|
+
{ header: "NAME", value: (r) => r.name ?? "", maxWidth: 24 },
|
|
1673
|
+
{ header: "LAST4", value: (r) => r.key_last4 ?? "" },
|
|
1674
|
+
{ header: "SCOPES", value: (r) => formatScopes(r.scopes), maxWidth: 28 },
|
|
1675
|
+
{ header: "STATUS", value: (r) => r.status ?? "" },
|
|
1676
|
+
{ header: "EXPIRES", value: (r) => formatDate3(r.expires_at), maxWidth: 12 },
|
|
1677
|
+
{ header: "LAST_USED", value: (r) => formatDate3(r.last_used_at), maxWidth: 12 }
|
|
1678
|
+
],
|
|
1679
|
+
ctx.color
|
|
1680
|
+
);
|
|
1681
|
+
const active = keys.filter((k) => k.status === "active").length;
|
|
1682
|
+
process.stdout.write(dim(`${keys.length} key(s), ${active} active.`, ctx.color) + "\n");
|
|
1683
|
+
}
|
|
1684
|
+
function registerEnterpriseKeysList(parent) {
|
|
1685
|
+
parent.command("list").description("List your company API keys (masked)").action(async (_flags, command) => {
|
|
1686
|
+
const ctx = resolveContext(command);
|
|
1687
|
+
const globals = command.optsWithGlobals();
|
|
1688
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1689
|
+
await runKeysList(ctx, key);
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/commands/enterprise/keys/rotate.ts
|
|
1694
|
+
var ENTERPRISE_KEY_EXPIRY_DAYS = [30, 60, 90];
|
|
1695
|
+
function validateExpiryDays(raw) {
|
|
1696
|
+
if (raw === void 0) return void 0;
|
|
1697
|
+
if (!ENTERPRISE_KEY_EXPIRY_DAYS.includes(raw)) {
|
|
1698
|
+
throw new CliError(
|
|
1699
|
+
`Invalid --expiry-days ${raw}. Allowed: ${ENTERPRISE_KEY_EXPIRY_DAYS.join(", ")}`,
|
|
1700
|
+
ExitCode.InvalidArgument
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
return raw;
|
|
1704
|
+
}
|
|
1705
|
+
async function runKeysRotate(ctx, apiKey, encId, flags) {
|
|
1706
|
+
const trimmed = encId.trim();
|
|
1707
|
+
if (!trimmed) throw new CliError("enc_id must not be empty", ExitCode.InvalidArgument);
|
|
1708
|
+
const expiryDays = validateExpiryDays(flags.expiryDays);
|
|
1709
|
+
const requestBody = {};
|
|
1710
|
+
if (expiryDays !== void 0) requestBody.expiry_days = expiryDays;
|
|
1711
|
+
const { body } = await enterprisePost(
|
|
1712
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1713
|
+
`/keys/${encodeURIComponent(trimmed)}/rotate`,
|
|
1714
|
+
requestBody
|
|
1715
|
+
);
|
|
1716
|
+
const issued = unwrapDataResponse(body);
|
|
1717
|
+
if (ctx.format === "json") {
|
|
1718
|
+
printJson(issued);
|
|
1719
|
+
} else {
|
|
1720
|
+
const plaintext = issued.api_key ?? "";
|
|
1721
|
+
const shown = flags.reveal ? plaintext : maskKey(plaintext);
|
|
1722
|
+
const lines = [
|
|
1723
|
+
`New API key: ${sanitizeForTerminal(shown)}`,
|
|
1724
|
+
`enc_id: ${sanitizeForTerminal(issued.enc_id ?? "")}`,
|
|
1725
|
+
`scopes: ${sanitizeForTerminal((issued.scopes ?? []).join(","))}`,
|
|
1726
|
+
`expires_at: ${sanitizeForTerminal(issued.expires_at ?? "")}`
|
|
1727
|
+
];
|
|
1728
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1729
|
+
if (!flags.reveal) {
|
|
1730
|
+
process.stdout.write(dim("Re-run with --reveal to print the full key once.", ctx.color) + "\n");
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
printWarn(
|
|
1734
|
+
`The previous key is now invalid. Update your ${API_KEY_ENV_VAR} env var / credential store, or run "wport enterprise login" with the new key.`,
|
|
1735
|
+
ctx.color
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
function registerEnterpriseKeysRotate(parent) {
|
|
1739
|
+
parent.command("rotate <enc_id>").description("Rotate an API key in place (issues a new key, invalidates the old one)").option("--expiry-days <n>", "new key lifetime in days (30 | 60 | 90; default 90)", (v) => Number(v)).option("--reveal", "print the full new key (default masks all but the last 4 chars)").action(async (encId, flags, command) => {
|
|
1740
|
+
const ctx = resolveContext(command);
|
|
1741
|
+
const globals = command.optsWithGlobals();
|
|
1742
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1743
|
+
await runKeysRotate(ctx, key, encId, flags);
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/commands/enterprise/keys/index.ts
|
|
1748
|
+
function registerEnterpriseKeysCommand(parent) {
|
|
1749
|
+
const keys = parent.command("keys").description("List and rotate your company API keys");
|
|
1750
|
+
registerEnterpriseKeysList(keys);
|
|
1751
|
+
registerEnterpriseKeysRotate(keys);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/commands/enterprise/company/view.ts
|
|
1755
|
+
var COMPANY_STATUS_LABELS = {
|
|
1756
|
+
0: "not_submitted",
|
|
1757
|
+
1: "pending_review",
|
|
1758
|
+
2: "approved",
|
|
1759
|
+
3: "rejected"
|
|
1760
|
+
};
|
|
1761
|
+
function formatCompanyStatus(status) {
|
|
1762
|
+
if (status === void 0) return "";
|
|
1763
|
+
return COMPANY_STATUS_LABELS[status] ?? String(status);
|
|
1764
|
+
}
|
|
1765
|
+
function formatPhone(company) {
|
|
1766
|
+
const code = company.phone_code ?? void 0;
|
|
1767
|
+
const number = company.phone_number ?? void 0;
|
|
1768
|
+
if (!code && !number) return void 0;
|
|
1769
|
+
return [code, number].filter(Boolean).join(" ");
|
|
1770
|
+
}
|
|
1771
|
+
function formatCapital(company) {
|
|
1772
|
+
if (company.capital_amount === null || company.capital_amount === void 0) return void 0;
|
|
1773
|
+
if (company.capital_show_status === 0) return "not displayed";
|
|
1774
|
+
return String(company.capital_amount);
|
|
1775
|
+
}
|
|
1776
|
+
function formatAddress(company) {
|
|
1777
|
+
return company.address ?? void 0;
|
|
1778
|
+
}
|
|
1779
|
+
var DETAIL_FIELDS2 = [
|
|
1780
|
+
"name",
|
|
1781
|
+
"uniform_number",
|
|
1782
|
+
"status",
|
|
1783
|
+
"website",
|
|
1784
|
+
"phone",
|
|
1785
|
+
"address",
|
|
1786
|
+
"capital",
|
|
1787
|
+
"logo_url"
|
|
1788
|
+
];
|
|
1789
|
+
function renderDetailLines2(company) {
|
|
1790
|
+
const pad = Math.max(...DETAIL_FIELDS2.map((f) => f.length)) + 1;
|
|
1791
|
+
const derived = {
|
|
1792
|
+
name: company.name,
|
|
1793
|
+
uniform_number: company.uniform_number ?? void 0,
|
|
1794
|
+
status: formatCompanyStatus(company.status),
|
|
1795
|
+
website: company.website ?? void 0,
|
|
1796
|
+
phone: formatPhone(company),
|
|
1797
|
+
address: formatAddress(company),
|
|
1798
|
+
capital: formatCapital(company),
|
|
1799
|
+
logo_url: company.logo_url ?? void 0
|
|
1800
|
+
};
|
|
1801
|
+
const lines = [];
|
|
1802
|
+
for (const field of DETAIL_FIELDS2) {
|
|
1803
|
+
const value = derived[field];
|
|
1804
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
1805
|
+
lines.push(`${(field + ":").padEnd(pad + 1)}${sanitizeForTerminal(value)}`);
|
|
1806
|
+
}
|
|
1807
|
+
return lines;
|
|
1808
|
+
}
|
|
1809
|
+
async function runCompanyView(ctx, apiKey, flags) {
|
|
1810
|
+
const { body } = await enterpriseGet(
|
|
1811
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1812
|
+
"/company"
|
|
1813
|
+
);
|
|
1814
|
+
const company = unwrapDataResponse(body);
|
|
1815
|
+
if (flags.fields) {
|
|
1816
|
+
printJson(pickPaths(company, parseFieldsList(flags.fields)));
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (ctx.format === "json") {
|
|
1820
|
+
printJson(company);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
process.stdout.write(renderDetailLines2(company).join("\n") + "\n");
|
|
1824
|
+
}
|
|
1825
|
+
function registerEnterpriseCompanyView(parent) {
|
|
1826
|
+
parent.command("view").description("View your company information").option("--fields <list>", "output selected fields as JSON (comma-separated dotted paths)").action(async (flags, command) => {
|
|
1827
|
+
const ctx = resolveContext(command);
|
|
1828
|
+
const globals = command.optsWithGlobals();
|
|
1829
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
1830
|
+
await runCompanyView(ctx, key, flags);
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/commands/enterprise/company/update.ts
|
|
1835
|
+
var import_node_crypto5 = require("crypto");
|
|
1836
|
+
|
|
1837
|
+
// src/commands/enterprise/company/types.ts
|
|
1838
|
+
var BASIC_FIELDS = [
|
|
1839
|
+
"name",
|
|
1840
|
+
"industry_category_code",
|
|
1841
|
+
"phone_code",
|
|
1842
|
+
"phone_number",
|
|
1843
|
+
"employee_count_range_code",
|
|
1844
|
+
"capital_amount",
|
|
1845
|
+
"capital_show_status",
|
|
1846
|
+
"website",
|
|
1847
|
+
"area_code",
|
|
1848
|
+
"address"
|
|
1849
|
+
];
|
|
1850
|
+
var DESCRIPTION_FIELDS = ["description", "products_services", "latest_news"];
|
|
1851
|
+
var FORBIDDEN_FIELDS = [
|
|
1852
|
+
"uniform_number",
|
|
1853
|
+
"banner_url",
|
|
1854
|
+
"photo_1",
|
|
1855
|
+
"photo_2",
|
|
1856
|
+
"photo_3",
|
|
1857
|
+
"video_1",
|
|
1858
|
+
"video_2",
|
|
1859
|
+
"video_3",
|
|
1860
|
+
"foundation_date",
|
|
1861
|
+
"representative",
|
|
1862
|
+
"directors",
|
|
1863
|
+
"milestones",
|
|
1864
|
+
"awards",
|
|
1865
|
+
"qa"
|
|
1866
|
+
];
|
|
1867
|
+
|
|
1868
|
+
// src/commands/enterprise/company/update.ts
|
|
1869
|
+
var REQUIRED_BASIC_FIELDS = [
|
|
1870
|
+
"name",
|
|
1871
|
+
"industry_category_code",
|
|
1872
|
+
"phone_code",
|
|
1873
|
+
"phone_number",
|
|
1874
|
+
"area_code",
|
|
1875
|
+
"address"
|
|
1876
|
+
];
|
|
1877
|
+
var CONTRACT_REQUIRED_GET_FIELDS = [
|
|
1878
|
+
"industry_category_code",
|
|
1879
|
+
"area_code",
|
|
1880
|
+
"employee_count_range_code",
|
|
1881
|
+
"capital_amount",
|
|
1882
|
+
"capital_show_status",
|
|
1883
|
+
"latest_news",
|
|
1884
|
+
"uniform_number"
|
|
1885
|
+
];
|
|
1886
|
+
async function buildCompanyUpdatePayloads(input, ctx, apiKey) {
|
|
1887
|
+
const forbiddenFound = Object.keys(input).filter((k) => FORBIDDEN_FIELDS.includes(k));
|
|
1888
|
+
if (forbiddenFound.length > 0) {
|
|
1889
|
+
throw new InvalidArgumentError(`Field(s) not writable via CLI: ${forbiddenFound.join(", ")}`);
|
|
1890
|
+
}
|
|
1891
|
+
const writableFields = /* @__PURE__ */ new Set([...BASIC_FIELDS, ...DESCRIPTION_FIELDS]);
|
|
1892
|
+
const unknownFound = Object.keys(input).filter((k) => !writableFields.has(k));
|
|
1893
|
+
if (unknownFound.length > 0) {
|
|
1894
|
+
throw new InvalidArgumentError(`Unknown field(s): ${unknownFound.join(", ")}`);
|
|
1895
|
+
}
|
|
1896
|
+
const inputHasBasicField = BASIC_FIELDS.some((f) => f in input);
|
|
1897
|
+
const inputHasDescriptionField = DESCRIPTION_FIELDS.some((f) => f in input);
|
|
1898
|
+
if (!inputHasBasicField && !inputHasDescriptionField) {
|
|
1899
|
+
throw new InvalidArgumentError("No writable company fields provided");
|
|
1900
|
+
}
|
|
1901
|
+
const { body } = await enterpriseGet(
|
|
1902
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
1903
|
+
"/company"
|
|
1904
|
+
);
|
|
1905
|
+
const current = unwrapDataResponse(body);
|
|
1906
|
+
const missingContractFields = CONTRACT_REQUIRED_GET_FIELDS.filter((f) => !(f in current));
|
|
1907
|
+
if (missingContractFields.length > 0) {
|
|
1908
|
+
throw new CliError(
|
|
1909
|
+
`Backend contract appears broken: GET /company response is missing field(s): ${missingContractFields.join(", ")}`,
|
|
1910
|
+
ExitCode.ServerOrNetworkError
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
let basic = null;
|
|
1914
|
+
if (inputHasBasicField) {
|
|
1915
|
+
const merged = {};
|
|
1916
|
+
for (const field of BASIC_FIELDS) {
|
|
1917
|
+
merged[field] = field in input ? input[field] : current[field];
|
|
1918
|
+
}
|
|
1919
|
+
const missingRequired = REQUIRED_BASIC_FIELDS.filter((f) => {
|
|
1920
|
+
const v = merged[f];
|
|
1921
|
+
return v === null || v === void 0 || v === "";
|
|
1922
|
+
});
|
|
1923
|
+
if (missingRequired.length > 0) {
|
|
1924
|
+
const [first] = missingRequired;
|
|
1925
|
+
throw new InvalidArgumentError(
|
|
1926
|
+
`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(", ")})` : "")
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
basic = merged;
|
|
1930
|
+
}
|
|
1931
|
+
let descriptions = null;
|
|
1932
|
+
if (inputHasDescriptionField) {
|
|
1933
|
+
descriptions = {};
|
|
1934
|
+
for (const field of DESCRIPTION_FIELDS) {
|
|
1935
|
+
if (field in input) descriptions[field] = input[field];
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return { basic, descriptions };
|
|
1939
|
+
}
|
|
1940
|
+
function isDescriptionsPartialFailureBody(body) {
|
|
1941
|
+
return !!body && typeof body === "object" && Array.isArray(body.updated_sections) && typeof body.failed_section === "string";
|
|
1942
|
+
}
|
|
1943
|
+
function printCompanyResult(company, format) {
|
|
1944
|
+
if (format === "json") {
|
|
1945
|
+
printJson(company);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
process.stdout.write(`Updated company: ${company.name ?? ""}
|
|
1949
|
+
`);
|
|
1950
|
+
}
|
|
1951
|
+
function resolveUpdateIdempotencyKeys(needsBasic, needsDescriptions, flags) {
|
|
1952
|
+
const bothNeeded = needsBasic && needsDescriptions;
|
|
1953
|
+
if (bothNeeded && flags.idempotencyKey !== void 0) {
|
|
1954
|
+
throw new InvalidArgumentError(
|
|
1955
|
+
"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"
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
basicKey: needsBasic ? flags.basicIdempotencyKey ?? flags.idempotencyKey ?? (0, import_node_crypto5.randomUUID)() : void 0,
|
|
1960
|
+
descriptionsKey: needsDescriptions ? flags.descriptionsIdempotencyKey ?? flags.idempotencyKey ?? (0, import_node_crypto5.randomUUID)() : void 0
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
async function runCompanyUpdate(ctx, apiKey, source, idempotencyFlags, options = {}) {
|
|
1964
|
+
const input = readJsonObject(source, options);
|
|
1965
|
+
const needsBasic = BASIC_FIELDS.some((f) => f in input);
|
|
1966
|
+
const needsDescriptions = DESCRIPTION_FIELDS.some((f) => f in input);
|
|
1967
|
+
const { basicKey, descriptionsKey } = resolveUpdateIdempotencyKeys(needsBasic, needsDescriptions, idempotencyFlags);
|
|
1968
|
+
const payloads = await buildCompanyUpdatePayloads(input, ctx, apiKey);
|
|
1969
|
+
const requestOpts = { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey };
|
|
1970
|
+
let basicResultCompany;
|
|
1971
|
+
if (needsBasic) {
|
|
1972
|
+
const { body } = await enterprisePatch(requestOpts, "/company/basic", payloads.basic, {
|
|
1973
|
+
idempotencyKey: basicKey
|
|
1974
|
+
});
|
|
1975
|
+
basicResultCompany = unwrapDataResponse(body);
|
|
1976
|
+
}
|
|
1977
|
+
if (!needsDescriptions) {
|
|
1978
|
+
printCompanyResult(basicResultCompany, ctx.format);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
const { body } = await enterprisePatch(
|
|
1983
|
+
requestOpts,
|
|
1984
|
+
"/company/descriptions",
|
|
1985
|
+
payloads.descriptions,
|
|
1986
|
+
{ idempotencyKey: descriptionsKey }
|
|
1987
|
+
);
|
|
1988
|
+
const result = unwrapDataResponse(body);
|
|
1989
|
+
printCompanyResult(result.company, ctx.format);
|
|
1990
|
+
} catch (err) {
|
|
1991
|
+
if (!needsBasic) {
|
|
1992
|
+
throw err;
|
|
1993
|
+
}
|
|
1994
|
+
const descriptionsBody = err instanceof ServerClientHttpError ? err.body : void 0;
|
|
1995
|
+
const partial = isDescriptionsPartialFailureBody(descriptionsBody) ? descriptionsBody : void 0;
|
|
1996
|
+
if (ctx.format === "json") {
|
|
1997
|
+
printJson({
|
|
1998
|
+
basic_updated: true,
|
|
1999
|
+
basic_company: basicResultCompany,
|
|
2000
|
+
descriptions_error: partial ? { updated_sections: partial.updated_sections, failed_section: partial.failed_section } : { message: err instanceof Error ? err.message : String(err) }
|
|
2001
|
+
});
|
|
2002
|
+
} else {
|
|
2003
|
+
process.stdout.write("Basic company info was updated successfully.\n");
|
|
2004
|
+
if (partial) {
|
|
2005
|
+
process.stdout.write(
|
|
2006
|
+
`Updated sections before failure: ${(partial.updated_sections ?? []).join(", ")}; failed section: ${partial.failed_section}
|
|
2007
|
+
`
|
|
2008
|
+
);
|
|
2009
|
+
} else {
|
|
2010
|
+
process.stdout.write(
|
|
2011
|
+
`Descriptions update failed entirely: ${err instanceof Error ? err.message : String(err)}
|
|
2012
|
+
`
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
throw err;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
function registerEnterpriseCompanyUpdate(parent) {
|
|
2020
|
+
parent.command("update").description(
|
|
2021
|
+
"Update your company basic info and/or descriptions from a JSON file (uniform_number is read-only and cannot be changed via this command)"
|
|
2022
|
+
).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(
|
|
2023
|
+
"--descriptions-idempotency-key <key>",
|
|
2024
|
+
"two-section update: idempotency key for PATCH /company/descriptions"
|
|
2025
|
+
).action(async (flags, command) => {
|
|
2026
|
+
const ctx = resolveContext(command);
|
|
2027
|
+
const globals = command.optsWithGlobals();
|
|
2028
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2029
|
+
await runCompanyUpdate(
|
|
2030
|
+
ctx,
|
|
2031
|
+
key,
|
|
2032
|
+
flags.file,
|
|
2033
|
+
{
|
|
2034
|
+
idempotencyKey: flags.idempotencyKey,
|
|
2035
|
+
basicIdempotencyKey: flags.basicIdempotencyKey,
|
|
2036
|
+
descriptionsIdempotencyKey: flags.descriptionsIdempotencyKey
|
|
2037
|
+
},
|
|
2038
|
+
{ timeoutMs: ctx.timeoutMs }
|
|
2039
|
+
);
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/commands/enterprise/company/logo.ts
|
|
2044
|
+
var import_node_crypto6 = require("crypto");
|
|
2045
|
+
var import_node_fs7 = require("fs");
|
|
2046
|
+
var import_node_path3 = require("path");
|
|
2047
|
+
var EXTENSION_TO_CONTENT_TYPE = {
|
|
2048
|
+
".png": "image/png",
|
|
2049
|
+
".jpg": "image/jpeg",
|
|
2050
|
+
".jpeg": "image/jpeg"
|
|
2051
|
+
};
|
|
2052
|
+
var COMPANY_LOGO_MAX_SIZE = 2 * 1024 * 1024;
|
|
2053
|
+
function inspectLocalFile(path) {
|
|
2054
|
+
if (!(0, import_node_fs7.existsSync)(path)) {
|
|
2055
|
+
throw new InvalidArgumentError(`File not found: ${path}`);
|
|
2056
|
+
}
|
|
2057
|
+
const stat = (0, import_node_fs7.statSync)(path);
|
|
2058
|
+
if (!stat.isFile()) {
|
|
2059
|
+
throw new InvalidArgumentError(`Not a regular file: ${path}`);
|
|
2060
|
+
}
|
|
2061
|
+
const ext = (0, import_node_path3.extname)(path).toLowerCase();
|
|
2062
|
+
const contentType = EXTENSION_TO_CONTENT_TYPE[ext];
|
|
2063
|
+
if (!contentType) {
|
|
2064
|
+
throw new InvalidArgumentError(
|
|
2065
|
+
`Unsupported file extension "${ext || "(none)"}" \u2014 allowed: ${Object.keys(EXTENSION_TO_CONTENT_TYPE).join(", ")}`
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
if (stat.size <= 0) {
|
|
2069
|
+
throw new InvalidArgumentError(`File is empty: ${path}`);
|
|
2070
|
+
}
|
|
2071
|
+
if (stat.size > COMPANY_LOGO_MAX_SIZE) {
|
|
2072
|
+
throw new InvalidArgumentError(`File too large: ${stat.size} bytes (max ${COMPANY_LOGO_MAX_SIZE} bytes / 2MB)`);
|
|
2073
|
+
}
|
|
2074
|
+
const bytes = (0, import_node_fs7.readFileSync)(path);
|
|
2075
|
+
return { contentType, fileSize: stat.size, bytes };
|
|
2076
|
+
}
|
|
2077
|
+
function resolveLogoIdempotencyKeys(flags) {
|
|
2078
|
+
if (flags.idempotencyKey) {
|
|
2079
|
+
return {
|
|
2080
|
+
presignKey: `${flags.idempotencyKey}-presign`,
|
|
2081
|
+
confirmKey: `${flags.idempotencyKey}-confirm`
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
return { presignKey: (0, import_node_crypto6.randomUUID)(), confirmKey: (0, import_node_crypto6.randomUUID)() };
|
|
2085
|
+
}
|
|
2086
|
+
function printLogoResult(result, format) {
|
|
2087
|
+
if (format === "json") {
|
|
2088
|
+
printJson(result);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
process.stdout.write(`Updated logo: ${result.logo_url}
|
|
2092
|
+
`);
|
|
2093
|
+
}
|
|
2094
|
+
async function runCompanyLogoUpload(ctx, apiKey, path, idempotencyFlags) {
|
|
2095
|
+
const { contentType, fileSize, bytes } = inspectLocalFile(path);
|
|
2096
|
+
const { presignKey, confirmKey } = resolveLogoIdempotencyKeys(idempotencyFlags);
|
|
2097
|
+
const requestOpts = { baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey };
|
|
2098
|
+
const { body: presignBody } = await enterprisePost(
|
|
2099
|
+
requestOpts,
|
|
2100
|
+
"/company/logo/presign",
|
|
2101
|
+
{ content_type: contentType, file_size: fileSize },
|
|
2102
|
+
{ idempotencyKey: presignKey }
|
|
2103
|
+
);
|
|
2104
|
+
const presign = unwrapDataResponse(presignBody);
|
|
2105
|
+
const putRequest = new Request(presign.upload_url, {
|
|
2106
|
+
method: "PUT",
|
|
2107
|
+
headers: { "Content-Type": contentType },
|
|
2108
|
+
body: bytes
|
|
2109
|
+
});
|
|
2110
|
+
const putResponse = await fetchWithTimeout(putRequest, ctx.timeoutMs);
|
|
2111
|
+
if (!putResponse.ok) {
|
|
2112
|
+
throw new NetworkError(`Failed to upload file to S3: HTTP ${putResponse.status}`);
|
|
2113
|
+
}
|
|
2114
|
+
const { body: confirmBody } = await enterprisePost(
|
|
2115
|
+
requestOpts,
|
|
2116
|
+
"/company/logo/confirm",
|
|
2117
|
+
{ s3_key: presign.s3_key },
|
|
2118
|
+
{ idempotencyKey: confirmKey }
|
|
2119
|
+
);
|
|
2120
|
+
const result = unwrapDataResponse(confirmBody);
|
|
2121
|
+
printLogoResult(result, ctx.format);
|
|
2122
|
+
}
|
|
2123
|
+
function registerEnterpriseCompanyLogo(parent) {
|
|
2124
|
+
const logo = parent.command("logo").description("Manage your company logo");
|
|
2125
|
+
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) => {
|
|
2126
|
+
const ctx = resolveContext(command);
|
|
2127
|
+
const globals = command.optsWithGlobals();
|
|
2128
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2129
|
+
await runCompanyLogoUpload(ctx, key, path, { idempotencyKey: flags.idempotencyKey });
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// src/commands/enterprise/company/index.ts
|
|
2134
|
+
function registerEnterpriseCompanyCommand(parent) {
|
|
2135
|
+
const company = parent.command("company").description("View and manage your company information");
|
|
2136
|
+
registerEnterpriseCompanyView(company);
|
|
2137
|
+
registerEnterpriseCompanyUpdate(company);
|
|
2138
|
+
registerEnterpriseCompanyLogo(company);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/commands/enterprise/talents/list.ts
|
|
2142
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
2143
|
+
var MINIMAL_LIST_FIELDS2 = ["enc_resume_id", "candidate_name", "applied_job_title", "applied_at"];
|
|
2144
|
+
function formatDate4(value) {
|
|
2145
|
+
return value ? String(value).slice(0, 10) : "";
|
|
2146
|
+
}
|
|
2147
|
+
async function runEnterpriseTalentsList(ctx, apiKey, flags) {
|
|
2148
|
+
if (flags.fields && flags.minimal) {
|
|
2149
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
2150
|
+
}
|
|
2151
|
+
const { body } = await enterpriseGet(
|
|
2152
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2153
|
+
"/talents",
|
|
2154
|
+
{
|
|
2155
|
+
tab: flags.tab,
|
|
2156
|
+
enc_job_id: flags.job,
|
|
2157
|
+
keyword: flags.keyword,
|
|
2158
|
+
start_date: flags.from,
|
|
2159
|
+
end_date: flags.to,
|
|
2160
|
+
currentPage: flags.page,
|
|
2161
|
+
pageSize: flags.pageSize ?? DEFAULT_PAGE_SIZE
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
const paged = asPaginatedBody(body);
|
|
2165
|
+
const projection = flags.minimal ? MINIMAL_LIST_FIELDS2 : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
2166
|
+
if (projection || ctx.format === "json") {
|
|
2167
|
+
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
printTable(
|
|
2171
|
+
paged.data,
|
|
2172
|
+
[
|
|
2173
|
+
{ header: "RESUME_ID", value: (r) => (r.enc_resume_id ?? "").slice(0, 14) },
|
|
2174
|
+
{ header: "CANDIDATE", value: (r) => r.candidate_name ?? "", maxWidth: 20 },
|
|
2175
|
+
{ header: "EDUCATION", value: (r) => r.highest_education ?? "", maxWidth: 18 },
|
|
2176
|
+
{ header: "EXPERIENCE", value: (r) => r.latest_experience ?? "", maxWidth: 28 },
|
|
2177
|
+
{ header: "APPLIED_JOB", value: (r) => r.applied_job_title ?? "", maxWidth: 24 },
|
|
2178
|
+
{ header: "APPLIED_AT", value: (r) => formatDate4(r.applied_at), maxWidth: 12 },
|
|
2179
|
+
{ header: "VIEWED", value: (r) => r.is_viewed ? "yes" : "no" }
|
|
2180
|
+
],
|
|
2181
|
+
ctx.color
|
|
2182
|
+
);
|
|
2183
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} applicants).`;
|
|
2184
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise talents list --page ${paged.currentPage + 1}` : "";
|
|
2185
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
2186
|
+
}
|
|
2187
|
+
function registerEnterpriseTalentsList(parent) {
|
|
2188
|
+
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) => {
|
|
2189
|
+
const ctx = resolveContext(command);
|
|
2190
|
+
const globals = command.optsWithGlobals();
|
|
2191
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2192
|
+
await runEnterpriseTalentsList(ctx, key, flags);
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/commands/enterprise/talents/view.ts
|
|
2197
|
+
async function runEnterpriseTalentsView(ctx, apiKey, encResumeId, flags) {
|
|
2198
|
+
const trimmed = encResumeId.trim();
|
|
2199
|
+
if (!trimmed) {
|
|
2200
|
+
throw new CliError("enc_resume_id must not be empty", ExitCode.InvalidArgument);
|
|
2201
|
+
}
|
|
2202
|
+
const { body } = await enterpriseGet(
|
|
2203
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2204
|
+
`/talents/${encodeURIComponent(trimmed)}`
|
|
2205
|
+
);
|
|
2206
|
+
const resume = unwrapDataResponse(body);
|
|
2207
|
+
if (flags.fields) {
|
|
2208
|
+
printJson(pickPaths(resume, parseFieldsList(flags.fields)));
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
printJson(resume);
|
|
2212
|
+
}
|
|
2213
|
+
function registerEnterpriseTalentsView(parent) {
|
|
2214
|
+
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) => {
|
|
2215
|
+
const ctx = resolveContext(command);
|
|
2216
|
+
const globals = command.optsWithGlobals();
|
|
2217
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2218
|
+
await runEnterpriseTalentsView(ctx, key, encResumeId, flags);
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// src/commands/enterprise/talents/respond.ts
|
|
2223
|
+
var import_node_crypto7 = require("crypto");
|
|
2224
|
+
function resolveRespondBody(flags, options = {}) {
|
|
2225
|
+
const hasBody = flags.body !== void 0;
|
|
2226
|
+
const hasBodyFile = flags.bodyFile !== void 0;
|
|
2227
|
+
if (hasBody === hasBodyFile) {
|
|
2228
|
+
throw new CliError("Provide exactly one of --body or --body-file", ExitCode.InvalidArgument);
|
|
2229
|
+
}
|
|
2230
|
+
if (hasBody) {
|
|
2231
|
+
if (!flags.body.trim()) throw new CliError("--body must not be empty", ExitCode.InvalidArgument);
|
|
2232
|
+
return flags.body;
|
|
2233
|
+
}
|
|
2234
|
+
return readTextInput(flags.bodyFile, { timeoutMs: options.timeoutMs });
|
|
2235
|
+
}
|
|
2236
|
+
async function runEnterpriseTalentsRespond(ctx, apiKey, encResumeId, flags, idempotencyKey) {
|
|
2237
|
+
const trimmedId = encResumeId.trim();
|
|
2238
|
+
if (!trimmedId) {
|
|
2239
|
+
throw new CliError("enc_resume_id must not be empty", ExitCode.InvalidArgument);
|
|
2240
|
+
}
|
|
2241
|
+
const subject = (flags.subject ?? "").trim();
|
|
2242
|
+
if (!subject) {
|
|
2243
|
+
throw new CliError("--subject is required and must not be empty", ExitCode.InvalidArgument);
|
|
2244
|
+
}
|
|
2245
|
+
const body = resolveRespondBody(flags, { timeoutMs: ctx.timeoutMs });
|
|
2246
|
+
const { body: respBody } = await enterprisePost(
|
|
2247
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2248
|
+
`/talents/${encodeURIComponent(trimmedId)}/respond`,
|
|
2249
|
+
{ subject, body },
|
|
2250
|
+
{ idempotencyKey }
|
|
2251
|
+
);
|
|
2252
|
+
const result = unwrapDataResponse(respBody);
|
|
2253
|
+
if (ctx.format === "json") {
|
|
2254
|
+
printJson(result);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
process.stdout.write(`Replied to ${result.enc_resume_id ?? trimmedId}; sent at ${result.sent_at ?? "(unknown)"}.
|
|
2258
|
+
`);
|
|
2259
|
+
}
|
|
2260
|
+
function registerEnterpriseTalentsRespond(parent) {
|
|
2261
|
+
parent.command("respond <enc_resume_id>").description("Reply to an applicant (in-app message; inherits the company daily reply quota)").requiredOption("--subject <subject>", "message subject (max 200)").option("--body <text>", "message body text (max 5000)").option("--body-file <path>", 'read message body from a file, or "-" for stdin').option("--idempotency-key <key>", "reuse across retries to avoid duplicate sends (default: a fresh UUID)").action(async (encResumeId, flags, command) => {
|
|
2262
|
+
const ctx = resolveContext(command);
|
|
2263
|
+
const globals = command.optsWithGlobals();
|
|
2264
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2265
|
+
await runEnterpriseTalentsRespond(ctx, key, encResumeId, flags, flags.idempotencyKey ?? (0, import_node_crypto7.randomUUID)());
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/commands/enterprise/talents/index.ts
|
|
2270
|
+
function registerEnterpriseTalentsCommand(parent) {
|
|
2271
|
+
const talents = parent.command("talents").description("Browse and respond to applicants in your talent pool");
|
|
2272
|
+
registerEnterpriseTalentsList(talents);
|
|
2273
|
+
registerEnterpriseTalentsView(talents);
|
|
2274
|
+
registerEnterpriseTalentsRespond(talents);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// src/commands/enterprise/campaigns/create.ts
|
|
2278
|
+
var import_node_crypto8 = require("crypto");
|
|
2279
|
+
async function runCampaignCreate(ctx, apiKey, source, idempotencyKey) {
|
|
2280
|
+
const campaignBody = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
2281
|
+
const { body } = await enterprisePost(
|
|
2282
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2283
|
+
"/campaigns",
|
|
2284
|
+
campaignBody,
|
|
2285
|
+
{ idempotencyKey }
|
|
2286
|
+
);
|
|
2287
|
+
const created = unwrapDataResponse(body);
|
|
2288
|
+
if (ctx.format === "json") {
|
|
2289
|
+
printJson(created);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
process.stdout.write(`Created campaign: ${created.enc_id ?? ""}
|
|
2293
|
+
`);
|
|
2294
|
+
}
|
|
2295
|
+
function registerEnterpriseCampaignsCreate(parent) {
|
|
2296
|
+
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) => {
|
|
2297
|
+
const ctx = resolveContext(command);
|
|
2298
|
+
const globals = command.optsWithGlobals();
|
|
2299
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2300
|
+
await runCampaignCreate(ctx, key, flags.file, flags.idempotencyKey ?? (0, import_node_crypto8.randomUUID)());
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// src/commands/enterprise/campaigns/list.ts
|
|
2305
|
+
var STATUS_MAP2 = { open: 1, closed: 0 };
|
|
2306
|
+
var MINIMAL_LIST_FIELDS3 = ["enc_id", "name", "status", "job_count"];
|
|
2307
|
+
function mapStatusFlag2(raw) {
|
|
2308
|
+
if (raw === void 0) return void 0;
|
|
2309
|
+
if (Object.prototype.hasOwnProperty.call(STATUS_MAP2, raw)) return STATUS_MAP2[raw];
|
|
2310
|
+
throw new CliError(`Invalid --status "${raw}". Allowed: ${Object.keys(STATUS_MAP2).join(", ")}`, ExitCode.InvalidArgument);
|
|
2311
|
+
}
|
|
2312
|
+
function formatStatus2(status) {
|
|
2313
|
+
if (status === 1) return "open";
|
|
2314
|
+
if (status === 0) return "closed";
|
|
2315
|
+
return status === void 0 ? "" : String(status);
|
|
2316
|
+
}
|
|
2317
|
+
function formatCount2(value) {
|
|
2318
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "\u2014";
|
|
2319
|
+
}
|
|
2320
|
+
async function runEnterpriseCampaignsList(ctx, apiKey, flags) {
|
|
2321
|
+
if (flags.fields && flags.minimal) {
|
|
2322
|
+
throw new CliError("Use either --fields or --minimal, not both", ExitCode.InvalidArgument);
|
|
2323
|
+
}
|
|
2324
|
+
const { body } = await enterpriseGet(
|
|
2325
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2326
|
+
"/campaigns",
|
|
2327
|
+
{
|
|
2328
|
+
currentPage: flags.page,
|
|
2329
|
+
pageSize: flags.pageSize,
|
|
2330
|
+
keyword: flags.keyword,
|
|
2331
|
+
status: mapStatusFlag2(flags.status)
|
|
2332
|
+
}
|
|
2333
|
+
);
|
|
2334
|
+
const paged = asPaginatedBody(body);
|
|
2335
|
+
const projection = flags.minimal ? MINIMAL_LIST_FIELDS3 : flags.fields ? parseFieldsList(flags.fields) : void 0;
|
|
2336
|
+
if (projection || ctx.format === "json") {
|
|
2337
|
+
printJson(projection ? { ...paged, data: paged.data.map((row) => pickPaths(row, projection)) } : paged);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
printTable(
|
|
2341
|
+
paged.data,
|
|
2342
|
+
[
|
|
2343
|
+
{ header: "ENC_ID", value: (r) => (r.enc_id ?? "").slice(0, 14) },
|
|
2344
|
+
{ header: "NAME", value: (r) => r.name ?? "", maxWidth: 32 },
|
|
2345
|
+
{ header: "STATUS", value: (r) => formatStatus2(r.status) },
|
|
2346
|
+
{ header: "JOBS", value: (r) => formatCount2(r.job_count) },
|
|
2347
|
+
{ header: "PV", value: (r) => formatCount2(r.pv) },
|
|
2348
|
+
{ header: "VISITORS", value: (r) => formatCount2(r.visitors_count) }
|
|
2349
|
+
],
|
|
2350
|
+
ctx.color
|
|
2351
|
+
);
|
|
2352
|
+
const head = `Showing page ${paged.currentPage}/${paged.totalPages} (${paged.data.length} of ${paged.totalCount} campaigns).`;
|
|
2353
|
+
const hint = paged.totalPages > paged.currentPage ? ` Next: wport enterprise campaigns list --page ${paged.currentPage + 1}` : "";
|
|
2354
|
+
process.stdout.write(dim(head + hint, ctx.color) + "\n");
|
|
2355
|
+
}
|
|
2356
|
+
function registerEnterpriseCampaignsList(parent) {
|
|
2357
|
+
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) => {
|
|
2358
|
+
const ctx = resolveContext(command);
|
|
2359
|
+
const globals = command.optsWithGlobals();
|
|
2360
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2361
|
+
await runEnterpriseCampaignsList(ctx, key, flags);
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/commands/enterprise/campaigns/lifecycle.ts
|
|
2366
|
+
var import_node_crypto9 = require("crypto");
|
|
2367
|
+
async function runCampaignTransition(ctx, apiKey, encId, action, idempotencyKey) {
|
|
2368
|
+
const trimmed = requireEncId(encId);
|
|
2369
|
+
const { body } = await enterprisePatch(
|
|
2370
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2371
|
+
`/campaigns/${encodeURIComponent(trimmed)}/${action}`,
|
|
2372
|
+
{},
|
|
2373
|
+
{ idempotencyKey }
|
|
2374
|
+
);
|
|
2375
|
+
const result = unwrapDataResponse(body);
|
|
2376
|
+
if (ctx.format === "json") {
|
|
2377
|
+
printJson(result);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const verb = action === "publish" ? "Published" : "Unpublished";
|
|
2381
|
+
process.stdout.write(`${verb} campaign: ${result.enc_id ?? trimmed}
|
|
2382
|
+
`);
|
|
2383
|
+
}
|
|
2384
|
+
function registerEnterpriseCampaignsPublish(parent) {
|
|
2385
|
+
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) => {
|
|
2386
|
+
const ctx = resolveContext(command);
|
|
2387
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
2388
|
+
await runCampaignTransition(ctx, key, encId, "publish", flags.idempotencyKey ?? (0, import_node_crypto9.randomUUID)());
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
function registerEnterpriseCampaignsUnpublish(parent) {
|
|
2392
|
+
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) => {
|
|
2393
|
+
const ctx = resolveContext(command);
|
|
2394
|
+
const { key } = resolveApiKey(command.optsWithGlobals().apiKey);
|
|
2395
|
+
await runCampaignTransition(ctx, key, encId, "unpublish", flags.idempotencyKey ?? (0, import_node_crypto9.randomUUID)());
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// src/commands/enterprise/campaigns/update.ts
|
|
2400
|
+
var import_node_crypto10 = require("crypto");
|
|
2401
|
+
async function runCampaignUpdate(ctx, apiKey, encId, source, idempotencyKey) {
|
|
2402
|
+
const trimmed = requireEncId(encId);
|
|
2403
|
+
const campaignBody = readJsonObject(source, { timeoutMs: ctx.timeoutMs });
|
|
2404
|
+
const { body } = await enterprisePatch(
|
|
2405
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2406
|
+
`/campaigns/${encodeURIComponent(trimmed)}`,
|
|
2407
|
+
campaignBody,
|
|
2408
|
+
{ idempotencyKey }
|
|
2409
|
+
);
|
|
2410
|
+
const updated = unwrapDataResponse(body);
|
|
2411
|
+
if (ctx.format === "json") {
|
|
2412
|
+
printJson(updated);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
process.stdout.write(`Updated campaign: ${updated.enc_id ?? trimmed}
|
|
2416
|
+
`);
|
|
2417
|
+
}
|
|
2418
|
+
function registerEnterpriseCampaignsUpdate(parent) {
|
|
2419
|
+
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) => {
|
|
2420
|
+
const ctx = resolveContext(command);
|
|
2421
|
+
const globals = command.optsWithGlobals();
|
|
2422
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2423
|
+
await runCampaignUpdate(ctx, key, encId, flags.file, flags.idempotencyKey ?? (0, import_node_crypto10.randomUUID)());
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/commands/enterprise/campaigns/view.ts
|
|
2428
|
+
async function runEnterpriseCampaignsView(ctx, apiKey, encId, flags) {
|
|
2429
|
+
const trimmed = encId.trim();
|
|
2430
|
+
if (!trimmed) {
|
|
2431
|
+
throw new CliError("enc_id must not be empty", ExitCode.InvalidArgument);
|
|
2432
|
+
}
|
|
2433
|
+
const { body } = await enterpriseGet(
|
|
2434
|
+
{ baseUrl: ctx.baseUrl, locale: ctx.locale, timeoutMs: ctx.timeoutMs, apiKey },
|
|
2435
|
+
`/campaigns/${encodeURIComponent(trimmed)}`
|
|
2436
|
+
);
|
|
2437
|
+
const campaign = unwrapDataResponse(body);
|
|
2438
|
+
if (flags.fields) {
|
|
2439
|
+
printJson(pickPaths(campaign, parseFieldsList(flags.fields)));
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
printJson(campaign);
|
|
2443
|
+
}
|
|
2444
|
+
function registerEnterpriseCampaignsView(parent) {
|
|
2445
|
+
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) => {
|
|
2446
|
+
const ctx = resolveContext(command);
|
|
2447
|
+
const globals = command.optsWithGlobals();
|
|
2448
|
+
const { key } = resolveApiKey(globals.apiKey);
|
|
2449
|
+
await runEnterpriseCampaignsView(ctx, key, encId, flags);
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/commands/enterprise/campaigns/index.ts
|
|
2454
|
+
function registerEnterpriseCampaignsCommand(parent) {
|
|
2455
|
+
const campaigns = parent.command("campaigns").description("Manage your recruitment campaigns");
|
|
2456
|
+
registerEnterpriseCampaignsList(campaigns);
|
|
2457
|
+
registerEnterpriseCampaignsView(campaigns);
|
|
2458
|
+
registerEnterpriseCampaignsCreate(campaigns);
|
|
2459
|
+
registerEnterpriseCampaignsUpdate(campaigns);
|
|
2460
|
+
registerEnterpriseCampaignsPublish(campaigns);
|
|
2461
|
+
registerEnterpriseCampaignsUnpublish(campaigns);
|
|
1289
2462
|
}
|
|
1290
2463
|
|
|
1291
2464
|
// src/commands/enterprise/index.ts
|
|
@@ -1294,12 +2467,17 @@ function registerEnterpriseCommand(program2) {
|
|
|
1294
2467
|
registerEnterpriseLogin(enterprise);
|
|
1295
2468
|
registerEnterpriseLogout(enterprise);
|
|
1296
2469
|
registerEnterpriseWhoami(enterprise);
|
|
2470
|
+
registerEnterpriseUsage(enterprise);
|
|
1297
2471
|
registerEnterpriseJobsCommand(enterprise);
|
|
2472
|
+
registerEnterpriseKeysCommand(enterprise);
|
|
2473
|
+
registerEnterpriseCompanyCommand(enterprise);
|
|
2474
|
+
registerEnterpriseTalentsCommand(enterprise);
|
|
2475
|
+
registerEnterpriseCampaignsCommand(enterprise);
|
|
1298
2476
|
}
|
|
1299
2477
|
|
|
1300
2478
|
// src/index.ts
|
|
1301
2479
|
var program = new import_commander.Command();
|
|
1302
|
-
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.
|
|
2480
|
+
program.name("wport").description("wport CLI \u2014 terminal interface to the W101 Talent Search Hub public API").version("0.5.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));
|
|
1303
2481
|
registerJobsCommand(program);
|
|
1304
2482
|
registerConfigCommand(program);
|
|
1305
2483
|
registerDoctorCommand(program);
|