@tplog/zendcli 0.1.1 → 0.2.3
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 +142 -0
- package/dist/cli.js +198 -25
- package/package.json +9 -1
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# zendcli
|
|
2
|
+
|
|
3
|
+
Minimal Zendesk CLI for listing tickets and reading ticket comment threads.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @tplog/zendcli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
### Option 1: interactive setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
zend configure
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Credentials are stored locally in:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
~/.zendcli/config.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The CLI writes this file with restricted permissions.
|
|
26
|
+
|
|
27
|
+
### Option 2: environment variables
|
|
28
|
+
|
|
29
|
+
This is the recommended option for temporary or CI usage.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export ZENDESK_SUBDOMAIN="your-subdomain"
|
|
33
|
+
export ZENDESK_EMAIL="you@example.com"
|
|
34
|
+
export ZENDESK_API_TOKEN="your_zendesk_api_token"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Environment variables take precedence over the config file.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
zend --help
|
|
43
|
+
zend tickets --help
|
|
44
|
+
zend email --help
|
|
45
|
+
zend follower --help
|
|
46
|
+
zend comments --help
|
|
47
|
+
zend tickets --limit 10
|
|
48
|
+
zend tickets --status open --limit 20
|
|
49
|
+
zend email user@example.com
|
|
50
|
+
zend email user@example.com --status unresolved
|
|
51
|
+
zend email user@example.com --status open,pending
|
|
52
|
+
zend follower
|
|
53
|
+
zend follower tp@dify.ai --limit 3
|
|
54
|
+
zend comments 12345
|
|
55
|
+
zend comments 12345 --type public
|
|
56
|
+
zend comments 12345 --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Development workflow
|
|
60
|
+
|
|
61
|
+
### Daily development
|
|
62
|
+
|
|
63
|
+
1. Create a feature branch from `main`
|
|
64
|
+
2. Develop locally
|
|
65
|
+
3. Commit as needed
|
|
66
|
+
4. Push branch to GitHub
|
|
67
|
+
5. Open a PR to `main`
|
|
68
|
+
6. Merge after CI passes
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git checkout main
|
|
74
|
+
git pull
|
|
75
|
+
git checkout -b feat/some-change
|
|
76
|
+
|
|
77
|
+
# work locally
|
|
78
|
+
npm ci
|
|
79
|
+
npm run build
|
|
80
|
+
|
|
81
|
+
git add .
|
|
82
|
+
git commit -m "feat: add some change"
|
|
83
|
+
git push -u origin feat/some-change
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Release workflow
|
|
87
|
+
|
|
88
|
+
This repository uses a safer release flow:
|
|
89
|
+
|
|
90
|
+
- normal merges to `main` do **not** publish automatically
|
|
91
|
+
- npm publishing happens only when a version tag is pushed
|
|
92
|
+
- the release tag must match `package.json` version exactly
|
|
93
|
+
|
|
94
|
+
### Publish a new version
|
|
95
|
+
|
|
96
|
+
1. Make sure `main` is in the state you want to release
|
|
97
|
+
2. Bump the version locally
|
|
98
|
+
3. Push `main` and the new tag
|
|
99
|
+
4. GitHub Actions publishes to npm
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git checkout main
|
|
103
|
+
git pull
|
|
104
|
+
npm version patch
|
|
105
|
+
git push origin main --tags
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or for a feature release:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm version minor
|
|
112
|
+
git push origin main --tags
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This creates tags like `v0.1.2`, and the publish workflow verifies that the tag matches `package.json`.
|
|
116
|
+
|
|
117
|
+
## CI/CD
|
|
118
|
+
|
|
119
|
+
- `CI`: runs on branch pushes, PRs to `main`, and pushes to `main`
|
|
120
|
+
- `Publish to npm`: runs only on `v*` tags or manual trigger
|
|
121
|
+
|
|
122
|
+
## Trusted publishing
|
|
123
|
+
|
|
124
|
+
The publish workflow is set up for npm trusted publishing via GitHub Actions OIDC.
|
|
125
|
+
|
|
126
|
+
Recommended setup on npm:
|
|
127
|
+
|
|
128
|
+
1. Go to the package settings on npm
|
|
129
|
+
2. Add a Trusted Publisher for GitHub Actions
|
|
130
|
+
3. Point it to:
|
|
131
|
+
- owner: `tplog`
|
|
132
|
+
- repo: `zendcli`
|
|
133
|
+
- workflow file: `publish.yml`
|
|
134
|
+
|
|
135
|
+
This avoids storing long-lived npm tokens in the repository.
|
|
136
|
+
|
|
137
|
+
## Security notes
|
|
138
|
+
|
|
139
|
+
- Never commit real Zendesk credentials
|
|
140
|
+
- Prefer environment variables for temporary use
|
|
141
|
+
- If a token is ever exposed, revoke and rotate it immediately
|
|
142
|
+
- Do not store npm publish credentials in the repo or in gitignored files
|
package/dist/cli.js
CHANGED
|
@@ -3478,37 +3478,48 @@ var import_os = require("os");
|
|
|
3478
3478
|
var import_path = require("path");
|
|
3479
3479
|
var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".zendcli");
|
|
3480
3480
|
var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
|
|
3481
|
+
function getEnvConfig() {
|
|
3482
|
+
return {
|
|
3483
|
+
subdomain: process.env.ZENDESK_SUBDOMAIN,
|
|
3484
|
+
email: process.env.ZENDESK_EMAIL,
|
|
3485
|
+
api_token: process.env.ZENDESK_API_TOKEN
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3481
3488
|
function loadConfig() {
|
|
3489
|
+
const envConfig = getEnvConfig();
|
|
3490
|
+
let fileConfig = {};
|
|
3482
3491
|
if ((0, import_fs.existsSync)(CONFIG_FILE)) {
|
|
3483
|
-
|
|
3492
|
+
fileConfig = JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf-8"));
|
|
3484
3493
|
}
|
|
3485
|
-
return {
|
|
3494
|
+
return {
|
|
3495
|
+
...fileConfig,
|
|
3496
|
+
...Object.fromEntries(Object.entries(envConfig).filter(([, value]) => value))
|
|
3497
|
+
};
|
|
3486
3498
|
}
|
|
3487
3499
|
function saveConfig(config) {
|
|
3488
|
-
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
3489
|
-
(0, import_fs.
|
|
3500
|
+
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
3501
|
+
(0, import_fs.chmodSync)(CONFIG_DIR, 448);
|
|
3502
|
+
(0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3503
|
+
(0, import_fs.chmodSync)(CONFIG_FILE, 384);
|
|
3490
3504
|
}
|
|
3491
3505
|
function getConfig() {
|
|
3492
3506
|
const config = loadConfig();
|
|
3493
3507
|
if (!config.subdomain || !config.email || !config.api_token) {
|
|
3494
|
-
console.error("Not configured. Run: zend configure");
|
|
3508
|
+
console.error("Not configured. Run: zend configure or set ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_TOKEN");
|
|
3495
3509
|
process.exit(1);
|
|
3496
3510
|
}
|
|
3497
3511
|
return config;
|
|
3498
3512
|
}
|
|
3499
3513
|
|
|
3500
3514
|
// src/api.ts
|
|
3501
|
-
|
|
3502
|
-
const {
|
|
3503
|
-
const url = new URL(`https://${subdomain}.zendesk.com${path}`);
|
|
3504
|
-
if (params) {
|
|
3505
|
-
for (const [key, value] of Object.entries(params)) {
|
|
3506
|
-
url.searchParams.set(key, String(value));
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3515
|
+
function getAuthHeader() {
|
|
3516
|
+
const { email, api_token } = getConfig();
|
|
3509
3517
|
const credentials = btoa(`${email}/token:${api_token}`);
|
|
3510
|
-
|
|
3511
|
-
|
|
3518
|
+
return `Basic ${credentials}`;
|
|
3519
|
+
}
|
|
3520
|
+
async function fetchJson(url) {
|
|
3521
|
+
const resp = await fetch(url, {
|
|
3522
|
+
headers: { Authorization: getAuthHeader() }
|
|
3512
3523
|
});
|
|
3513
3524
|
if (!resp.ok) {
|
|
3514
3525
|
const body = await resp.text();
|
|
@@ -3517,29 +3528,159 @@ async function apiGet(path, params) {
|
|
|
3517
3528
|
}
|
|
3518
3529
|
return resp.json();
|
|
3519
3530
|
}
|
|
3531
|
+
async function apiGet(path, params) {
|
|
3532
|
+
const { subdomain } = getConfig();
|
|
3533
|
+
const url = new URL(`https://${subdomain}.zendesk.com${path}`);
|
|
3534
|
+
if (params) {
|
|
3535
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3536
|
+
url.searchParams.set(key, String(value));
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return fetchJson(url.toString());
|
|
3540
|
+
}
|
|
3541
|
+
async function apiGetUrl(url) {
|
|
3542
|
+
return fetchJson(url);
|
|
3543
|
+
}
|
|
3520
3544
|
|
|
3521
3545
|
// src/cli.ts
|
|
3522
3546
|
var program2 = new Command();
|
|
3547
|
+
var VALID_TICKET_STATUSES = ["new", "open", "pending", "hold", "solved", "closed"];
|
|
3523
3548
|
program2.name("zend").description("Zendesk tickets & comments CLI").version("0.1.0");
|
|
3524
|
-
function prompt(question, defaultValue = ""
|
|
3549
|
+
function prompt(question, defaultValue = "") {
|
|
3525
3550
|
return new Promise((resolve) => {
|
|
3526
3551
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
3527
|
-
const suffix = defaultValue ? ` [${
|
|
3552
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
3528
3553
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
3529
3554
|
rl.close();
|
|
3530
3555
|
resolve(answer || defaultValue);
|
|
3531
3556
|
});
|
|
3532
3557
|
});
|
|
3533
3558
|
}
|
|
3559
|
+
function promptHidden(question, hasDefault = false) {
|
|
3560
|
+
return new Promise((resolve) => {
|
|
3561
|
+
const stdin = process.stdin;
|
|
3562
|
+
const stdout = process.stderr;
|
|
3563
|
+
let value = "";
|
|
3564
|
+
readline.emitKeypressEvents(stdin);
|
|
3565
|
+
if (stdin.isTTY) {
|
|
3566
|
+
stdin.setRawMode(true);
|
|
3567
|
+
}
|
|
3568
|
+
const suffix = hasDefault ? " [****]" : "";
|
|
3569
|
+
stdout.write(`${question}${suffix}: `);
|
|
3570
|
+
const onKeypress = (char, key) => {
|
|
3571
|
+
if (key.name === "return" || key.name === "enter") {
|
|
3572
|
+
stdout.write("\n");
|
|
3573
|
+
stdin.off("keypress", onKeypress);
|
|
3574
|
+
if (stdin.isTTY) {
|
|
3575
|
+
stdin.setRawMode(false);
|
|
3576
|
+
}
|
|
3577
|
+
resolve(value);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (key.ctrl && key.name === "c") {
|
|
3581
|
+
stdout.write("\n");
|
|
3582
|
+
stdin.off("keypress", onKeypress);
|
|
3583
|
+
if (stdin.isTTY) {
|
|
3584
|
+
stdin.setRawMode(false);
|
|
3585
|
+
}
|
|
3586
|
+
process.exit(130);
|
|
3587
|
+
}
|
|
3588
|
+
if (key.name === "backspace") {
|
|
3589
|
+
value = value.slice(0, -1);
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
if (char) {
|
|
3593
|
+
value += char;
|
|
3594
|
+
}
|
|
3595
|
+
};
|
|
3596
|
+
stdin.on("keypress", onKeypress);
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
function parseStatusFilter(input) {
|
|
3600
|
+
if (!input) return [];
|
|
3601
|
+
const statuses = input.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
3602
|
+
const invalid = statuses.filter((status) => status !== "unresolved" && status !== "all" && !VALID_TICKET_STATUSES.includes(status));
|
|
3603
|
+
if (invalid.length > 0) {
|
|
3604
|
+
console.error(`Invalid status value(s): ${invalid.join(", ")}`);
|
|
3605
|
+
console.error(`Allowed values: unresolved, all, ${VALID_TICKET_STATUSES.join(", ")}`);
|
|
3606
|
+
process.exit(1);
|
|
3607
|
+
}
|
|
3608
|
+
if (statuses.includes("all")) return [];
|
|
3609
|
+
if (statuses.includes("unresolved")) return ["new", "open", "pending", "hold"];
|
|
3610
|
+
return statuses;
|
|
3611
|
+
}
|
|
3612
|
+
function buildSearchQuery(base, rawStatus, statusFilter, canUseSearchStatus) {
|
|
3613
|
+
let query = base;
|
|
3614
|
+
if (rawStatus === "unresolved") {
|
|
3615
|
+
query += " status<solved";
|
|
3616
|
+
} else if (canUseSearchStatus && statusFilter.length === 1) {
|
|
3617
|
+
query += ` status:${statusFilter[0]}`;
|
|
3618
|
+
}
|
|
3619
|
+
return query;
|
|
3620
|
+
}
|
|
3621
|
+
function printTickets(tickets, meta) {
|
|
3622
|
+
for (const t of tickets) {
|
|
3623
|
+
const status = (t.status || "").padEnd(8);
|
|
3624
|
+
const subject = t.subject || "(no subject)";
|
|
3625
|
+
console.log(`[${t.id}] ${status} ${subject}`);
|
|
3626
|
+
console.log(meta(t));
|
|
3627
|
+
console.log();
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async function findUserByEmail(email) {
|
|
3631
|
+
const data = await apiGet("/api/v2/users/search.json", {
|
|
3632
|
+
query: email
|
|
3633
|
+
});
|
|
3634
|
+
const user = (data.users || []).find((item) => item.email?.toLowerCase() === email.toLowerCase()) || data.users?.[0];
|
|
3635
|
+
if (!user) {
|
|
3636
|
+
console.error(`No Zendesk user found for email: ${email}`);
|
|
3637
|
+
process.exit(1);
|
|
3638
|
+
}
|
|
3639
|
+
return user;
|
|
3640
|
+
}
|
|
3641
|
+
async function findFollowerTickets(email, rawStatus, limit, sort) {
|
|
3642
|
+
const user = await findUserByEmail(email);
|
|
3643
|
+
const statusFilter = parseStatusFilter(rawStatus);
|
|
3644
|
+
const canUseSearchStatus = rawStatus === "unresolved" || statusFilter.length === 1;
|
|
3645
|
+
let url = new URL(`https://${getConfig().subdomain}.zendesk.com/api/v2/search.json`).toString();
|
|
3646
|
+
const params = new URLSearchParams({
|
|
3647
|
+
query: buildSearchQuery("type:ticket", rawStatus, statusFilter, canUseSearchStatus),
|
|
3648
|
+
sort_by: "updated_at",
|
|
3649
|
+
sort_order: sort,
|
|
3650
|
+
per_page: "100"
|
|
3651
|
+
});
|
|
3652
|
+
url += `?${params.toString()}`;
|
|
3653
|
+
const matches = [];
|
|
3654
|
+
while (url && matches.length < limit) {
|
|
3655
|
+
const data = await apiGetUrl(url);
|
|
3656
|
+
let tickets = data.results || [];
|
|
3657
|
+
if (statusFilter.length > 0 && rawStatus !== "unresolved" && !canUseSearchStatus) {
|
|
3658
|
+
tickets = tickets.filter((ticket) => statusFilter.includes((ticket.status || "").toLowerCase()));
|
|
3659
|
+
}
|
|
3660
|
+
for (const ticket of tickets) {
|
|
3661
|
+
const followers = ticket.follower_ids || [];
|
|
3662
|
+
if (followers.includes(user.id) && ticket.assignee_id !== user.id) {
|
|
3663
|
+
matches.push(ticket);
|
|
3664
|
+
}
|
|
3665
|
+
if (matches.length >= limit) {
|
|
3666
|
+
break;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
url = data.next_page || null;
|
|
3670
|
+
}
|
|
3671
|
+
return matches;
|
|
3672
|
+
}
|
|
3534
3673
|
program2.command("configure").description("Set up Zendesk credentials interactively").action(async () => {
|
|
3535
3674
|
const existing = loadConfig();
|
|
3536
3675
|
console.error("Zendesk CLI Configuration");
|
|
3537
3676
|
console.error("\u2500".repeat(30));
|
|
3538
3677
|
const subdomain = await prompt("Subdomain (xxx.zendesk.com)", existing.subdomain);
|
|
3539
3678
|
const email = await prompt("Email", existing.email);
|
|
3540
|
-
const
|
|
3679
|
+
const tokenInput = await promptHidden("API Token", Boolean(existing.api_token));
|
|
3680
|
+
const api_token = tokenInput || existing.api_token || "";
|
|
3541
3681
|
saveConfig({ subdomain, email, api_token });
|
|
3542
|
-
console.error("\nSaved to ~/.zendcli/config.json");
|
|
3682
|
+
console.error("\nSaved to ~/.zendcli/config.json with restricted permissions (0600)");
|
|
3683
|
+
console.error("Environment variables also work: ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_TOKEN");
|
|
3543
3684
|
});
|
|
3544
3685
|
program2.command("tickets").description("List tickets from Zendesk, sorted by updated_at").option("--status <status>", "Filter by status (new|open|pending|hold|solved|closed)").option("--limit <n>", "Max tickets to return", "20").option("--sort <order>", "Sort order (asc|desc)", "desc").action(async (opts) => {
|
|
3545
3686
|
const limit = parseInt(opts.limit, 10);
|
|
@@ -3552,13 +3693,45 @@ program2.command("tickets").description("List tickets from Zendesk, sorted by up
|
|
|
3552
3693
|
if (opts.status) {
|
|
3553
3694
|
tickets = tickets.filter((t) => t.status === opts.status);
|
|
3554
3695
|
}
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3696
|
+
printTickets(tickets.slice(0, limit), (ticket) => {
|
|
3697
|
+
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} created=${ticket.created_at}`;
|
|
3698
|
+
});
|
|
3699
|
+
});
|
|
3700
|
+
program2.command("email <email>").description("Find tickets for a requester email").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) => {
|
|
3701
|
+
const limit = parseInt(opts.limit, 10);
|
|
3702
|
+
const statusFilter = parseStatusFilter(opts.status);
|
|
3703
|
+
const canUseSearchStatus = opts.status === "unresolved" || statusFilter.length === 1;
|
|
3704
|
+
const query = buildSearchQuery(`type:ticket requester:${email}`, opts.status, statusFilter, canUseSearchStatus);
|
|
3705
|
+
const data = await apiGet("/api/v2/search.json", {
|
|
3706
|
+
query,
|
|
3707
|
+
sort_by: "updated_at",
|
|
3708
|
+
sort_order: opts.sort,
|
|
3709
|
+
per_page: 100
|
|
3710
|
+
});
|
|
3711
|
+
let tickets = data.results || [];
|
|
3712
|
+
if (statusFilter.length > 0 && opts.status !== "unresolved" && !canUseSearchStatus) {
|
|
3713
|
+
tickets = tickets.filter((ticket) => statusFilter.includes((ticket.status || "").toLowerCase()));
|
|
3561
3714
|
}
|
|
3715
|
+
tickets = tickets.slice(0, limit);
|
|
3716
|
+
if (opts.json) {
|
|
3717
|
+
console.log(JSON.stringify(tickets, null, 2));
|
|
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;
|
|
3731
|
+
}
|
|
3732
|
+
printTickets(tickets, (ticket) => {
|
|
3733
|
+
return ` priority=${ticket.priority ?? "-"} type=${ticket.type ?? "-"} follower=${targetEmail} assignee=${ticket.assignee_id ?? "-"} updated=${ticket.updated_at}`;
|
|
3734
|
+
});
|
|
3562
3735
|
});
|
|
3563
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) => {
|
|
3564
3737
|
const data = await apiGet(
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tplog/zendcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Minimal Zendesk CLI for tickets and comments",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/tplog/zendcli.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/tplog/zendcli#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/tplog/zendcli/issues"
|
|
12
|
+
},
|
|
5
13
|
"bin": {
|
|
6
14
|
"zend": "dist/cli.js"
|
|
7
15
|
},
|