@tplog/zendcli 0.1.1 → 0.2.2
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 +135 -0
- package/dist/cli.js +112 -9
- package/package.json +9 -1
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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 tickets --limit 10
|
|
43
|
+
zend tickets --status open --limit 20
|
|
44
|
+
zend email user@example.com
|
|
45
|
+
zend email user@example.com --status unresolved
|
|
46
|
+
zend email user@example.com --status open,pending
|
|
47
|
+
zend comments 12345
|
|
48
|
+
zend comments 12345 --type public
|
|
49
|
+
zend comments 12345 --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Development workflow
|
|
53
|
+
|
|
54
|
+
### Daily development
|
|
55
|
+
|
|
56
|
+
1. Create a feature branch from `main`
|
|
57
|
+
2. Develop locally
|
|
58
|
+
3. Commit as needed
|
|
59
|
+
4. Push branch to GitHub
|
|
60
|
+
5. Open a PR to `main`
|
|
61
|
+
6. Merge after CI passes
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git checkout main
|
|
67
|
+
git pull
|
|
68
|
+
git checkout -b feat/some-change
|
|
69
|
+
|
|
70
|
+
# work locally
|
|
71
|
+
npm ci
|
|
72
|
+
npm run build
|
|
73
|
+
|
|
74
|
+
git add .
|
|
75
|
+
git commit -m "feat: add some change"
|
|
76
|
+
git push -u origin feat/some-change
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Release workflow
|
|
80
|
+
|
|
81
|
+
This repository uses a safer release flow:
|
|
82
|
+
|
|
83
|
+
- normal merges to `main` do **not** publish automatically
|
|
84
|
+
- npm publishing happens only when a version tag is pushed
|
|
85
|
+
- the release tag must match `package.json` version exactly
|
|
86
|
+
|
|
87
|
+
### Publish a new version
|
|
88
|
+
|
|
89
|
+
1. Make sure `main` is in the state you want to release
|
|
90
|
+
2. Bump the version locally
|
|
91
|
+
3. Push `main` and the new tag
|
|
92
|
+
4. GitHub Actions publishes to npm
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git checkout main
|
|
96
|
+
git pull
|
|
97
|
+
npm version patch
|
|
98
|
+
git push origin main --tags
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or for a feature release:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm version minor
|
|
105
|
+
git push origin main --tags
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This creates tags like `v0.1.2`, and the publish workflow verifies that the tag matches `package.json`.
|
|
109
|
+
|
|
110
|
+
## CI/CD
|
|
111
|
+
|
|
112
|
+
- `CI`: runs on branch pushes, PRs to `main`, and pushes to `main`
|
|
113
|
+
- `Publish to npm`: runs only on `v*` tags or manual trigger
|
|
114
|
+
|
|
115
|
+
## Trusted publishing
|
|
116
|
+
|
|
117
|
+
The publish workflow is set up for npm trusted publishing via GitHub Actions OIDC.
|
|
118
|
+
|
|
119
|
+
Recommended setup on npm:
|
|
120
|
+
|
|
121
|
+
1. Go to the package settings on npm
|
|
122
|
+
2. Add a Trusted Publisher for GitHub Actions
|
|
123
|
+
3. Point it to:
|
|
124
|
+
- owner: `tplog`
|
|
125
|
+
- repo: `zendcli`
|
|
126
|
+
- workflow file: `publish.yml`
|
|
127
|
+
|
|
128
|
+
This avoids storing long-lived npm tokens in the repository.
|
|
129
|
+
|
|
130
|
+
## Security notes
|
|
131
|
+
|
|
132
|
+
- Never commit real Zendesk credentials
|
|
133
|
+
- Prefer environment variables for temporary use
|
|
134
|
+
- If a token is ever exposed, revoke and rotate it immediately
|
|
135
|
+
- Do not store npm publish credentials in the repo or in gitignored files
|
package/dist/cli.js
CHANGED
|
@@ -3478,20 +3478,34 @@ 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;
|
|
@@ -3520,26 +3534,82 @@ async function apiGet(path, params) {
|
|
|
3520
3534
|
|
|
3521
3535
|
// src/cli.ts
|
|
3522
3536
|
var program2 = new Command();
|
|
3537
|
+
var VALID_TICKET_STATUSES = ["new", "open", "pending", "hold", "solved", "closed"];
|
|
3523
3538
|
program2.name("zend").description("Zendesk tickets & comments CLI").version("0.1.0");
|
|
3524
|
-
function prompt(question, defaultValue = ""
|
|
3539
|
+
function prompt(question, defaultValue = "") {
|
|
3525
3540
|
return new Promise((resolve) => {
|
|
3526
3541
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
3527
|
-
const suffix = defaultValue ? ` [${
|
|
3542
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
3528
3543
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
3529
3544
|
rl.close();
|
|
3530
3545
|
resolve(answer || defaultValue);
|
|
3531
3546
|
});
|
|
3532
3547
|
});
|
|
3533
3548
|
}
|
|
3549
|
+
function promptHidden(question, hasDefault = false) {
|
|
3550
|
+
return new Promise((resolve) => {
|
|
3551
|
+
const stdin = process.stdin;
|
|
3552
|
+
const stdout = process.stderr;
|
|
3553
|
+
let value = "";
|
|
3554
|
+
readline.emitKeypressEvents(stdin);
|
|
3555
|
+
if (stdin.isTTY) {
|
|
3556
|
+
stdin.setRawMode(true);
|
|
3557
|
+
}
|
|
3558
|
+
const suffix = hasDefault ? " [****]" : "";
|
|
3559
|
+
stdout.write(`${question}${suffix}: `);
|
|
3560
|
+
const onKeypress = (char, key) => {
|
|
3561
|
+
if (key.name === "return" || key.name === "enter") {
|
|
3562
|
+
stdout.write("\n");
|
|
3563
|
+
stdin.off("keypress", onKeypress);
|
|
3564
|
+
if (stdin.isTTY) {
|
|
3565
|
+
stdin.setRawMode(false);
|
|
3566
|
+
}
|
|
3567
|
+
resolve(value);
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
if (key.ctrl && key.name === "c") {
|
|
3571
|
+
stdout.write("\n");
|
|
3572
|
+
stdin.off("keypress", onKeypress);
|
|
3573
|
+
if (stdin.isTTY) {
|
|
3574
|
+
stdin.setRawMode(false);
|
|
3575
|
+
}
|
|
3576
|
+
process.exit(130);
|
|
3577
|
+
}
|
|
3578
|
+
if (key.name === "backspace") {
|
|
3579
|
+
value = value.slice(0, -1);
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (char) {
|
|
3583
|
+
value += char;
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
stdin.on("keypress", onKeypress);
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
function parseStatusFilter(input) {
|
|
3590
|
+
if (!input) return [];
|
|
3591
|
+
const statuses = input.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
3592
|
+
const invalid = statuses.filter((status) => status !== "unresolved" && status !== "all" && !VALID_TICKET_STATUSES.includes(status));
|
|
3593
|
+
if (invalid.length > 0) {
|
|
3594
|
+
console.error(`Invalid status value(s): ${invalid.join(", ")}`);
|
|
3595
|
+
console.error(`Allowed values: unresolved, all, ${VALID_TICKET_STATUSES.join(", ")}`);
|
|
3596
|
+
process.exit(1);
|
|
3597
|
+
}
|
|
3598
|
+
if (statuses.includes("all")) return [];
|
|
3599
|
+
if (statuses.includes("unresolved")) return ["new", "open", "pending", "hold"];
|
|
3600
|
+
return statuses;
|
|
3601
|
+
}
|
|
3534
3602
|
program2.command("configure").description("Set up Zendesk credentials interactively").action(async () => {
|
|
3535
3603
|
const existing = loadConfig();
|
|
3536
3604
|
console.error("Zendesk CLI Configuration");
|
|
3537
3605
|
console.error("\u2500".repeat(30));
|
|
3538
3606
|
const subdomain = await prompt("Subdomain (xxx.zendesk.com)", existing.subdomain);
|
|
3539
3607
|
const email = await prompt("Email", existing.email);
|
|
3540
|
-
const
|
|
3608
|
+
const tokenInput = await promptHidden("API Token", Boolean(existing.api_token));
|
|
3609
|
+
const api_token = tokenInput || existing.api_token || "";
|
|
3541
3610
|
saveConfig({ subdomain, email, api_token });
|
|
3542
|
-
console.error("\nSaved to ~/.zendcli/config.json");
|
|
3611
|
+
console.error("\nSaved to ~/.zendcli/config.json with restricted permissions (0600)");
|
|
3612
|
+
console.error("Environment variables also work: ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_TOKEN");
|
|
3543
3613
|
});
|
|
3544
3614
|
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
3615
|
const limit = parseInt(opts.limit, 10);
|
|
@@ -3560,6 +3630,39 @@ program2.command("tickets").description("List tickets from Zendesk, sorted by up
|
|
|
3560
3630
|
console.log();
|
|
3561
3631
|
}
|
|
3562
3632
|
});
|
|
3633
|
+
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) => {
|
|
3634
|
+
const limit = parseInt(opts.limit, 10);
|
|
3635
|
+
const statusFilter = parseStatusFilter(opts.status);
|
|
3636
|
+
const canUseSearchStatus = opts.status === "unresolved" || statusFilter.length === 1;
|
|
3637
|
+
let query = `type:ticket requester:${email}`;
|
|
3638
|
+
if (opts.status === "unresolved") {
|
|
3639
|
+
query += " status<solved";
|
|
3640
|
+
} else if (canUseSearchStatus && statusFilter.length === 1) {
|
|
3641
|
+
query += ` status:${statusFilter[0]}`;
|
|
3642
|
+
}
|
|
3643
|
+
const data = await apiGet("/api/v2/search.json", {
|
|
3644
|
+
query,
|
|
3645
|
+
sort_by: "updated_at",
|
|
3646
|
+
sort_order: opts.sort,
|
|
3647
|
+
per_page: 100
|
|
3648
|
+
});
|
|
3649
|
+
let tickets = data.results || [];
|
|
3650
|
+
if (statusFilter.length > 0 && opts.status !== "unresolved" && !canUseSearchStatus) {
|
|
3651
|
+
tickets = tickets.filter((ticket) => statusFilter.includes((ticket.status || "").toLowerCase()));
|
|
3652
|
+
}
|
|
3653
|
+
tickets = tickets.slice(0, limit);
|
|
3654
|
+
if (opts.json) {
|
|
3655
|
+
console.log(JSON.stringify(tickets, null, 2));
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
for (const t of tickets) {
|
|
3659
|
+
const status = (t.status || "").padEnd(8);
|
|
3660
|
+
const subject = t.subject || "(no subject)";
|
|
3661
|
+
console.log(`[${t.id}] ${status} ${subject}`);
|
|
3662
|
+
console.log(` priority=${t.priority ?? "-"} type=${t.type ?? "-"} requester=${email} updated=${t.updated_at}`);
|
|
3663
|
+
console.log();
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3563
3666
|
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
3667
|
const data = await apiGet(
|
|
3565
3668
|
`/api/v2/tickets/${ticketId}/comments.json`,
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tplog/zendcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
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
|
},
|