@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.
Files changed (3) hide show
  1. package/README.md +135 -0
  2. package/dist/cli.js +112 -9
  3. 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
- return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf-8"));
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.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
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 = "", hidden = false) {
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 ? ` [${hidden ? "****" : 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 api_token = await prompt("API Token", existing.api_token, true);
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.1.1",
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
  },