@tplog/zendcli 0.2.3 → 1.1.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/README.md +5 -5
- package/dist/cli.js +204 -143
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ This is the recommended option for temporary or CI usage.
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
export ZENDESK_SUBDOMAIN="your-subdomain"
|
|
33
|
-
export ZENDESK_EMAIL="
|
|
33
|
+
export ZENDESK_EMAIL="foo@example.com"
|
|
34
34
|
export ZENDESK_API_TOKEN="your_zendesk_api_token"
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -46,11 +46,11 @@ zend follower --help
|
|
|
46
46
|
zend comments --help
|
|
47
47
|
zend tickets --limit 10
|
|
48
48
|
zend tickets --status open --limit 20
|
|
49
|
-
zend email
|
|
50
|
-
zend email
|
|
51
|
-
zend email
|
|
49
|
+
zend email foo@example.com
|
|
50
|
+
zend email foo@example.com --status unresolved
|
|
51
|
+
zend email foo@example.com --status open,pending
|
|
52
52
|
zend follower
|
|
53
|
-
zend follower
|
|
53
|
+
zend follower foo@example.com --limit 3
|
|
54
54
|
zend comments 12345
|
|
55
55
|
zend comments 12345 --type public
|
|
56
56
|
zend comments 12345 --json
|
package/dist/cli.js
CHANGED
|
@@ -3512,29 +3512,42 @@ function getConfig() {
|
|
|
3512
3512
|
}
|
|
3513
3513
|
|
|
3514
3514
|
// src/api.ts
|
|
3515
|
-
|
|
3515
|
+
var ApiError = class extends Error {
|
|
3516
|
+
status;
|
|
3517
|
+
body;
|
|
3518
|
+
constructor(message, status, body) {
|
|
3519
|
+
super(message);
|
|
3520
|
+
this.name = "ApiError";
|
|
3521
|
+
this.status = status;
|
|
3522
|
+
this.body = body;
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
function authHeader() {
|
|
3516
3526
|
const { email, api_token } = getConfig();
|
|
3517
3527
|
const credentials = btoa(`${email}/token:${api_token}`);
|
|
3518
3528
|
return `Basic ${credentials}`;
|
|
3519
3529
|
}
|
|
3520
3530
|
async function fetchJson(url) {
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3531
|
+
let resp;
|
|
3532
|
+
try {
|
|
3533
|
+
resp = await fetch(url, {
|
|
3534
|
+
headers: { Authorization: authHeader() }
|
|
3535
|
+
});
|
|
3536
|
+
} catch (error) {
|
|
3537
|
+
const message = error instanceof Error ? error.message : "Network request failed";
|
|
3538
|
+
throw new ApiError(message);
|
|
3539
|
+
}
|
|
3524
3540
|
if (!resp.ok) {
|
|
3525
3541
|
const body = await resp.text();
|
|
3526
|
-
|
|
3527
|
-
process.exit(1);
|
|
3542
|
+
throw new ApiError(`HTTP ${resp.status}`, resp.status, body);
|
|
3528
3543
|
}
|
|
3529
3544
|
return resp.json();
|
|
3530
3545
|
}
|
|
3531
3546
|
async function apiGet(path, params) {
|
|
3532
3547
|
const { subdomain } = getConfig();
|
|
3533
3548
|
const url = new URL(`https://${subdomain}.zendesk.com${path}`);
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
url.searchParams.set(key, String(value));
|
|
3537
|
-
}
|
|
3549
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
3550
|
+
url.searchParams.set(key, String(value));
|
|
3538
3551
|
}
|
|
3539
3552
|
return fetchJson(url.toString());
|
|
3540
3553
|
}
|
|
@@ -3545,7 +3558,55 @@ async function apiGetUrl(url) {
|
|
|
3545
3558
|
// src/cli.ts
|
|
3546
3559
|
var program2 = new Command();
|
|
3547
3560
|
var VALID_TICKET_STATUSES = ["new", "open", "pending", "hold", "solved", "closed"];
|
|
3548
|
-
|
|
3561
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["configure", "follower", "comments", "help", "email", "ticket"]);
|
|
3562
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3563
|
+
var DIGITS_RE = /^\d+$/;
|
|
3564
|
+
var CliError = class extends Error {
|
|
3565
|
+
code;
|
|
3566
|
+
details;
|
|
3567
|
+
exitCode;
|
|
3568
|
+
constructor(code, message, details = {}, exitCode = 1) {
|
|
3569
|
+
super(message);
|
|
3570
|
+
this.name = "CliError";
|
|
3571
|
+
this.code = code;
|
|
3572
|
+
this.details = details;
|
|
3573
|
+
this.exitCode = exitCode;
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
program2.name("zend").description("Zendesk tickets CLI").version("2.0.0");
|
|
3577
|
+
function printJson(value) {
|
|
3578
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
3579
|
+
`);
|
|
3580
|
+
}
|
|
3581
|
+
function fail(code, message, details = {}, exitCode = 1) {
|
|
3582
|
+
printJson({ error: code, message, ...details });
|
|
3583
|
+
process.exit(exitCode);
|
|
3584
|
+
}
|
|
3585
|
+
function handleError(error) {
|
|
3586
|
+
if (error instanceof CliError) {
|
|
3587
|
+
fail(error.code, error.message, error.details, error.exitCode);
|
|
3588
|
+
}
|
|
3589
|
+
if (error instanceof ApiError) {
|
|
3590
|
+
if (error.status === 401) {
|
|
3591
|
+
fail("auth_failed", "401 Unauthorized", { status: 401 });
|
|
3592
|
+
}
|
|
3593
|
+
if (error.status === 404) {
|
|
3594
|
+
fail("not_found", "Resource not found", { status: 404 });
|
|
3595
|
+
}
|
|
3596
|
+
fail("api_error", error.body || error.message, error.status ? { status: error.status } : {});
|
|
3597
|
+
}
|
|
3598
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3599
|
+
fail("unknown_error", message);
|
|
3600
|
+
}
|
|
3601
|
+
function run(fn) {
|
|
3602
|
+
return async (...args) => {
|
|
3603
|
+
try {
|
|
3604
|
+
await fn(...args);
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
handleError(error);
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3549
3610
|
function prompt(question, defaultValue = "") {
|
|
3550
3611
|
return new Promise((resolve) => {
|
|
3551
3612
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -3562,199 +3623,199 @@ function promptHidden(question, hasDefault = false) {
|
|
|
3562
3623
|
const stdout = process.stderr;
|
|
3563
3624
|
let value = "";
|
|
3564
3625
|
readline.emitKeypressEvents(stdin);
|
|
3565
|
-
if (stdin.isTTY)
|
|
3566
|
-
|
|
3567
|
-
}
|
|
3568
|
-
const suffix = hasDefault ? " [****]" : "";
|
|
3569
|
-
stdout.write(`${question}${suffix}: `);
|
|
3626
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
3627
|
+
stdout.write(`${question}${hasDefault ? " [****]" : ""}: `);
|
|
3570
3628
|
const onKeypress = (char, key) => {
|
|
3571
3629
|
if (key.name === "return" || key.name === "enter") {
|
|
3572
3630
|
stdout.write("\n");
|
|
3573
3631
|
stdin.off("keypress", onKeypress);
|
|
3574
|
-
if (stdin.isTTY)
|
|
3575
|
-
stdin.setRawMode(false);
|
|
3576
|
-
}
|
|
3632
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
3577
3633
|
resolve(value);
|
|
3578
3634
|
return;
|
|
3579
3635
|
}
|
|
3580
3636
|
if (key.ctrl && key.name === "c") {
|
|
3581
3637
|
stdout.write("\n");
|
|
3582
3638
|
stdin.off("keypress", onKeypress);
|
|
3583
|
-
if (stdin.isTTY)
|
|
3584
|
-
stdin.setRawMode(false);
|
|
3585
|
-
}
|
|
3639
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
3586
3640
|
process.exit(130);
|
|
3587
3641
|
}
|
|
3588
3642
|
if (key.name === "backspace") {
|
|
3589
3643
|
value = value.slice(0, -1);
|
|
3590
3644
|
return;
|
|
3591
3645
|
}
|
|
3592
|
-
if (char)
|
|
3593
|
-
value += char;
|
|
3594
|
-
}
|
|
3646
|
+
if (char) value += char;
|
|
3595
3647
|
};
|
|
3596
3648
|
stdin.on("keypress", onKeypress);
|
|
3597
3649
|
});
|
|
3598
3650
|
}
|
|
3599
|
-
function parseStatusFilter(input) {
|
|
3600
|
-
if (!input) return [];
|
|
3651
|
+
function parseStatusFilter(input = "unresolved") {
|
|
3601
3652
|
const statuses = input.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
3602
3653
|
const invalid = statuses.filter((status) => status !== "unresolved" && status !== "all" && !VALID_TICKET_STATUSES.includes(status));
|
|
3603
3654
|
if (invalid.length > 0) {
|
|
3604
|
-
|
|
3605
|
-
console.error(`Allowed values: unresolved, all, ${VALID_TICKET_STATUSES.join(", ")}`);
|
|
3606
|
-
process.exit(1);
|
|
3655
|
+
throw new CliError("invalid_args", `Invalid status value(s): ${invalid.join(", ")}`, { input });
|
|
3607
3656
|
}
|
|
3608
3657
|
if (statuses.includes("all")) return [];
|
|
3609
3658
|
if (statuses.includes("unresolved")) return ["new", "open", "pending", "hold"];
|
|
3610
3659
|
return statuses;
|
|
3611
3660
|
}
|
|
3612
|
-
function
|
|
3613
|
-
|
|
3614
|
-
if (
|
|
3615
|
-
|
|
3616
|
-
} else if (canUseSearchStatus && statusFilter.length === 1) {
|
|
3617
|
-
query += ` status:${statusFilter[0]}`;
|
|
3661
|
+
function parseLimit(input = "20") {
|
|
3662
|
+
const limit = Number.parseInt(input, 10);
|
|
3663
|
+
if (!Number.isFinite(limit) || Number.isNaN(limit)) {
|
|
3664
|
+
throw new CliError("invalid_args", "limit must be an integer", { limit: input });
|
|
3618
3665
|
}
|
|
3619
|
-
|
|
3666
|
+
if (limit < 1 || limit > 100) {
|
|
3667
|
+
throw new CliError("invalid_args", "limit must be between 1 and 100", { limit });
|
|
3668
|
+
}
|
|
3669
|
+
return limit;
|
|
3620
3670
|
}
|
|
3621
|
-
function
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
const subject = t.subject || "(no subject)";
|
|
3625
|
-
console.log(`[${t.id}] ${status} ${subject}`);
|
|
3626
|
-
console.log(meta(t));
|
|
3627
|
-
console.log();
|
|
3671
|
+
function parseSort(input = "desc") {
|
|
3672
|
+
if (input !== "asc" && input !== "desc") {
|
|
3673
|
+
throw new CliError("invalid_args", "sort must be asc or desc", { sort: input });
|
|
3628
3674
|
}
|
|
3675
|
+
return input;
|
|
3676
|
+
}
|
|
3677
|
+
function buildSearchQuery(base, rawStatus, statusFilter) {
|
|
3678
|
+
if (rawStatus === "unresolved") return `${base} status<solved`;
|
|
3679
|
+
if (statusFilter.length === 1) return `${base} status:${statusFilter[0]}`;
|
|
3680
|
+
return base;
|
|
3681
|
+
}
|
|
3682
|
+
function filterStatuses(tickets, rawStatus, statusFilter) {
|
|
3683
|
+
if (rawStatus === "unresolved" || statusFilter.length <= 1) return tickets;
|
|
3684
|
+
return tickets.filter((ticket) => statusFilter.includes(String(ticket.status || "").toLowerCase()));
|
|
3629
3685
|
}
|
|
3630
3686
|
async function findUserByEmail(email) {
|
|
3631
|
-
const data = await apiGet("/api/v2/users/search.json", {
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
if (
|
|
3636
|
-
|
|
3637
|
-
process.exit(1);
|
|
3638
|
-
}
|
|
3639
|
-
return user;
|
|
3687
|
+
const data = await apiGet("/api/v2/users/search.json", { query: email });
|
|
3688
|
+
const users = data.users || [];
|
|
3689
|
+
const exact = users.find((user) => user.email?.toLowerCase() === email.toLowerCase());
|
|
3690
|
+
if (exact) return exact;
|
|
3691
|
+
if (users[0]) return users[0];
|
|
3692
|
+
throw new CliError("user_not_found", `No Zendesk user found for email: ${email}`, { email });
|
|
3640
3693
|
}
|
|
3641
|
-
async function
|
|
3694
|
+
async function fetchFollowerTickets(email, rawStatus, limit, sort) {
|
|
3642
3695
|
const user = await findUserByEmail(email);
|
|
3643
3696
|
const statusFilter = parseStatusFilter(rawStatus);
|
|
3644
|
-
const
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
sort_order: sort,
|
|
3650
|
-
per_page: "100"
|
|
3651
|
-
});
|
|
3652
|
-
url += `?${params.toString()}`;
|
|
3697
|
+
const baseUrl = new URL(`${apiBaseUrl()}/api/v2/search.json`);
|
|
3698
|
+
baseUrl.searchParams.set("query", buildSearchQuery("type:ticket", rawStatus, statusFilter));
|
|
3699
|
+
baseUrl.searchParams.set("sort_by", "updated_at");
|
|
3700
|
+
baseUrl.searchParams.set("sort_order", sort);
|
|
3701
|
+
baseUrl.searchParams.set("per_page", "100");
|
|
3653
3702
|
const matches = [];
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
tickets = tickets.filter((ticket) => statusFilter.includes((ticket.status || "").toLowerCase()));
|
|
3659
|
-
}
|
|
3703
|
+
let nextUrl = baseUrl.toString();
|
|
3704
|
+
while (nextUrl && matches.length < limit) {
|
|
3705
|
+
const data = await apiGetUrl(nextUrl);
|
|
3706
|
+
const tickets = filterStatuses(data.results || [], rawStatus, statusFilter);
|
|
3660
3707
|
for (const ticket of tickets) {
|
|
3661
|
-
|
|
3662
|
-
if (followers.includes(user.id) && ticket.assignee_id !== user.id) {
|
|
3708
|
+
if ((ticket.follower_ids || []).includes(user.id) && ticket.assignee_id !== user.id) {
|
|
3663
3709
|
matches.push(ticket);
|
|
3664
3710
|
}
|
|
3665
|
-
if (matches.length >= limit)
|
|
3666
|
-
break;
|
|
3667
|
-
}
|
|
3711
|
+
if (matches.length >= limit) break;
|
|
3668
3712
|
}
|
|
3669
|
-
|
|
3713
|
+
nextUrl = data.next_page || null;
|
|
3670
3714
|
}
|
|
3671
3715
|
return matches;
|
|
3672
3716
|
}
|
|
3673
|
-
|
|
3717
|
+
function apiBaseUrl() {
|
|
3718
|
+
const { subdomain } = getConfig();
|
|
3719
|
+
return `https://${subdomain}.zendesk.com`;
|
|
3720
|
+
}
|
|
3721
|
+
program2.command("configure").description("Set up Zendesk credentials interactively").action(run(async () => {
|
|
3674
3722
|
const existing = loadConfig();
|
|
3675
|
-
|
|
3676
|
-
|
|
3723
|
+
process.stderr.write("Zendesk CLI Configuration\n");
|
|
3724
|
+
process.stderr.write(`${"\u2500".repeat(30)}
|
|
3725
|
+
`);
|
|
3677
3726
|
const subdomain = await prompt("Subdomain (xxx.zendesk.com)", existing.subdomain);
|
|
3678
3727
|
const email = await prompt("Email", existing.email);
|
|
3679
3728
|
const tokenInput = await promptHidden("API Token", Boolean(existing.api_token));
|
|
3680
3729
|
const api_token = tokenInput || existing.api_token || "";
|
|
3681
3730
|
saveConfig({ subdomain, email, api_token });
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3731
|
+
printJson({ ok: true });
|
|
3732
|
+
}));
|
|
3733
|
+
var TICKET_SUMMARY_FIELDS = [
|
|
3734
|
+
"id",
|
|
3735
|
+
"subject",
|
|
3736
|
+
"description",
|
|
3737
|
+
"status",
|
|
3738
|
+
"priority",
|
|
3739
|
+
"created_at",
|
|
3740
|
+
"updated_at",
|
|
3741
|
+
"tags",
|
|
3742
|
+
"requester_id",
|
|
3743
|
+
"assignee_id",
|
|
3744
|
+
"collaborator_ids",
|
|
3745
|
+
"follower_ids",
|
|
3746
|
+
"organization_id",
|
|
3747
|
+
"group_id",
|
|
3748
|
+
"type",
|
|
3749
|
+
"via",
|
|
3750
|
+
"url"
|
|
3751
|
+
];
|
|
3752
|
+
function pickTicketFields(ticket) {
|
|
3753
|
+
const result = {};
|
|
3754
|
+
for (const key of TICKET_SUMMARY_FIELDS) {
|
|
3755
|
+
if (key in ticket) result[key] = ticket[key];
|
|
3695
3756
|
}
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3757
|
+
return result;
|
|
3758
|
+
}
|
|
3759
|
+
program2.command("ticket <id>").description("Get a single ticket").option("--raw", "Output full API response without field filtering").action(run(async (id, opts) => {
|
|
3760
|
+
if (!DIGITS_RE.test(id)) {
|
|
3761
|
+
throw new CliError("invalid_args", "ticket id must be numeric", { id });
|
|
3762
|
+
}
|
|
3763
|
+
try {
|
|
3764
|
+
const data = await apiGet(`/api/v2/tickets/${id}.json`);
|
|
3765
|
+
if (!data.ticket) {
|
|
3766
|
+
throw new CliError("not_found", `Ticket ${id} not found`, { id: Number(id) });
|
|
3767
|
+
}
|
|
3768
|
+
printJson(opts.raw ? data.ticket : pickTicketFields(data.ticket));
|
|
3769
|
+
} catch (error) {
|
|
3770
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
3771
|
+
fail("not_found", `Ticket ${id} not found`, { id: Number(id) });
|
|
3772
|
+
}
|
|
3773
|
+
throw error;
|
|
3774
|
+
}
|
|
3775
|
+
}));
|
|
3776
|
+
program2.command("email <email>").description("Find tickets for an assignee email").option("--status <status>", "unresolved|all|new|open|pending|hold|solved|closed or comma-separated list", "unresolved").option("--limit <n>", "Max tickets to return", "20").option("--sort <order>", "Sort order (asc|desc)", "desc").action(run(async (email, opts) => {
|
|
3777
|
+
const limit = parseLimit(opts.limit);
|
|
3778
|
+
const sort = parseSort(opts.sort);
|
|
3702
3779
|
const statusFilter = parseStatusFilter(opts.status);
|
|
3703
|
-
const
|
|
3704
|
-
const query = buildSearchQuery(`type:ticket requester:${email}`, opts.status, statusFilter, canUseSearchStatus);
|
|
3780
|
+
const query = buildSearchQuery(`type:ticket assignee:${email}`, opts.status, statusFilter);
|
|
3705
3781
|
const data = await apiGet("/api/v2/search.json", {
|
|
3706
3782
|
query,
|
|
3707
3783
|
sort_by: "updated_at",
|
|
3708
|
-
sort_order:
|
|
3784
|
+
sort_order: sort,
|
|
3709
3785
|
per_page: 100
|
|
3710
3786
|
});
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3787
|
+
const tickets = filterStatuses(data.results || [], opts.status, statusFilter).slice(0, limit);
|
|
3788
|
+
printJson(tickets);
|
|
3789
|
+
}));
|
|
3790
|
+
program2.command("follower <email>").description("Find tickets where the user is a follower but not the assignee").option("--status <status>", "unresolved|all|new|open|pending|hold|solved|closed or comma-separated list", "unresolved").option("--limit <n>", "Max tickets to return", "20").option("--sort <order>", "Sort order (asc|desc)", "desc").action(run(async (email, opts) => {
|
|
3791
|
+
const limit = parseLimit(opts.limit);
|
|
3792
|
+
const sort = parseSort(opts.sort);
|
|
3793
|
+
const tickets = await fetchFollowerTickets(email, opts.status, limit, sort);
|
|
3794
|
+
printJson(tickets);
|
|
3795
|
+
}));
|
|
3796
|
+
program2.command("comments <ticketId>").description("List comments for a ticket").option("--type <type>", "all|public|internal", "all").option("--sort <order>", "Sort order (asc|desc)", "asc").action(run(async (ticketId, opts) => {
|
|
3797
|
+
if (!DIGITS_RE.test(ticketId)) {
|
|
3798
|
+
throw new CliError("invalid_args", "ticket id must be numeric", { ticketId });
|
|
3714
3799
|
}
|
|
3715
|
-
|
|
3716
|
-
if (opts.
|
|
3717
|
-
|
|
3718
|
-
return;
|
|
3719
|
-
}
|
|
3720
|
-
printTickets(tickets, (ticket) => {
|
|
3721
|
-
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} requester=${email} updated=${ticket.updated_at}`;
|
|
3722
|
-
});
|
|
3723
|
-
});
|
|
3724
|
-
program2.command("follower [email]").description("Find tickets where the user is a follower but not the assignee").option("--status <status>", "Filter status: unresolved|all|new|open|pending|hold|solved|closed or comma-separated list", "unresolved").option("--limit <n>", "Max tickets to return", "20").option("--sort <order>", "Sort order (asc|desc)", "desc").option("--json", "Output raw JSON").action(async (email, opts) => {
|
|
3725
|
-
const targetEmail = email || getConfig().email;
|
|
3726
|
-
const limit = parseInt(opts.limit, 10);
|
|
3727
|
-
const tickets = await findFollowerTickets(targetEmail, opts.status, limit, opts.sort);
|
|
3728
|
-
if (opts.json) {
|
|
3729
|
-
console.log(JSON.stringify(tickets, null, 2));
|
|
3730
|
-
return;
|
|
3800
|
+
const sort = parseSort(opts.sort);
|
|
3801
|
+
if (!["all", "public", "internal"].includes(opts.type)) {
|
|
3802
|
+
throw new CliError("invalid_args", "type must be all, public, or internal", { type: opts.type });
|
|
3731
3803
|
}
|
|
3732
|
-
printTickets(tickets, (ticket) => {
|
|
3733
|
-
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} follower=${targetEmail} assignee=${ticket.assignee_id ?? "-"} updated=${ticket.updated_at}`;
|
|
3734
|
-
});
|
|
3735
|
-
});
|
|
3736
|
-
program2.command("comments <ticketId>").description("List comments/thread for a ticket (public=customer-facing, internal=agents only)").option("--type <type>", "Filter: all|public|internal", "all").option("--sort <order>", "Sort order (asc|desc)", "asc").option("--json", "Output raw JSON").action(async (ticketId, opts) => {
|
|
3737
3804
|
const data = await apiGet(
|
|
3738
3805
|
`/api/v2/tickets/${ticketId}/comments.json`,
|
|
3739
|
-
{ sort_order:
|
|
3806
|
+
{ sort_order: sort, per_page: 100 }
|
|
3740
3807
|
);
|
|
3741
3808
|
let comments = data.comments || [];
|
|
3742
|
-
if (opts.type === "public")
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
console.log(`--- [${label}] id=${c.id} author=${c.author_id} via=${via} ${c.created_at} ---`);
|
|
3755
|
-
const body = (c.plain_body || c.body || "").trim();
|
|
3756
|
-
console.log(body);
|
|
3757
|
-
console.log();
|
|
3758
|
-
}
|
|
3759
|
-
});
|
|
3809
|
+
if (opts.type === "public") comments = comments.filter((comment) => comment.public === true);
|
|
3810
|
+
if (opts.type === "internal") comments = comments.filter((comment) => comment.public === false);
|
|
3811
|
+
printJson(comments);
|
|
3812
|
+
}));
|
|
3813
|
+
function preprocessArgv(argv) {
|
|
3814
|
+
const firstArg = argv[2];
|
|
3815
|
+
if (!firstArg || firstArg.startsWith("-") || KNOWN_COMMANDS.has(firstArg)) return argv;
|
|
3816
|
+
if (EMAIL_RE.test(firstArg)) return [...argv.slice(0, 2), "email", ...argv.slice(2)];
|
|
3817
|
+
if (DIGITS_RE.test(firstArg)) return [...argv.slice(0, 2), "ticket", ...argv.slice(2)];
|
|
3818
|
+
return argv;
|
|
3819
|
+
}
|
|
3820
|
+
process.argv = preprocessArgv(process.argv);
|
|
3760
3821
|
program2.parse();
|