Skip to content

Commit b85fa21

Browse files
BjoernSchotteclaude
andcommitted
feat(jira): add watch/unwatch/watchers commands
- jira watch <key> - Start watching an issue - jira unwatch <key> - Stop watching an issue - jira watchers <key> - List watchers for an issue Added webhook server to roadmap as next phase (Bun HTTP server). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 31a0a7c commit b85fa21

3 files changed

Lines changed: 199 additions & 1 deletion

File tree

apps/cli/src/commands/jira.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ export async function handleJira(
9393
case "me":
9494
await handleMe(flags, opts);
9595
return;
96+
case "watch":
97+
await handleWatch(rest, flags, opts);
98+
return;
99+
case "unwatch":
100+
await handleUnwatch(rest, flags, opts);
101+
return;
102+
case "watchers":
103+
await handleWatchers(rest, flags, opts);
104+
return;
96105
default:
97106
output(jiraHelp(), opts);
98107
return;
@@ -3393,12 +3402,107 @@ Examples:
33933402
`;
33943403
}
33953404

3405+
// ============ Watch Commands ============
3406+
3407+
async function handleWatch(
3408+
args: string[],
3409+
flags: Record<string, string | boolean>,
3410+
opts: OutputOptions
3411+
): Promise<void> {
3412+
const [key] = args;
3413+
if (!key) {
3414+
fail(opts, 1, ERROR_CODES.USAGE, "Usage: jira watch <issue-key>");
3415+
return;
3416+
}
3417+
3418+
const client = await getClient(flags, opts);
3419+
const me = await client.getCurrentUser();
3420+
3421+
if (!me.accountId) {
3422+
fail(opts, 1, ERROR_CODES.AUTH, "Could not determine current user accountId.");
3423+
return;
3424+
}
3425+
3426+
await client.addWatcher(key, me.accountId);
3427+
3428+
if (opts.json) {
3429+
output({ schemaVersion: "1", watching: key, user: me.displayName }, opts);
3430+
} else {
3431+
output(`Now watching ${key}`, opts);
3432+
}
3433+
}
3434+
3435+
async function handleUnwatch(
3436+
args: string[],
3437+
flags: Record<string, string | boolean>,
3438+
opts: OutputOptions
3439+
): Promise<void> {
3440+
const [key] = args;
3441+
if (!key) {
3442+
fail(opts, 1, ERROR_CODES.USAGE, "Usage: jira unwatch <issue-key>");
3443+
return;
3444+
}
3445+
3446+
const client = await getClient(flags, opts);
3447+
const me = await client.getCurrentUser();
3448+
3449+
if (!me.accountId) {
3450+
fail(opts, 1, ERROR_CODES.AUTH, "Could not determine current user accountId.");
3451+
return;
3452+
}
3453+
3454+
await client.removeWatcher(key, me.accountId);
3455+
3456+
if (opts.json) {
3457+
output({ schemaVersion: "1", unwatched: key, user: me.displayName }, opts);
3458+
} else {
3459+
output(`Stopped watching ${key}`, opts);
3460+
}
3461+
}
3462+
3463+
async function handleWatchers(
3464+
args: string[],
3465+
flags: Record<string, string | boolean>,
3466+
opts: OutputOptions
3467+
): Promise<void> {
3468+
const [key] = args;
3469+
if (!key) {
3470+
fail(opts, 1, ERROR_CODES.USAGE, "Usage: jira watchers <issue-key>");
3471+
return;
3472+
}
3473+
3474+
const client = await getClient(flags, opts);
3475+
const result = await client.getWatchers(key);
3476+
3477+
if (opts.json) {
3478+
output({
3479+
schemaVersion: "1",
3480+
issue: key,
3481+
watchCount: result.watchCount,
3482+
isWatching: result.isWatching,
3483+
watchers: result.watchers.map((w) => ({
3484+
accountId: w.accountId,
3485+
displayName: w.displayName,
3486+
email: w.emailAddress,
3487+
})),
3488+
}, opts);
3489+
} else {
3490+
output(`Watchers for ${key} (${result.watchCount}):`, opts);
3491+
if (result.isWatching) {
3492+
output(` (You are watching this issue)`, opts);
3493+
}
3494+
for (const w of result.watchers) {
3495+
output(` - ${w.displayName}${w.emailAddress ? ` <${w.emailAddress}>` : ""}`, opts);
3496+
}
3497+
}
3498+
}
3499+
33963500
function jiraHelp(): string {
33973501
return `atlcli jira <command>
33983502
33993503
Commands:
34003504
project Project operations (list, get, create, types)
3401-
issue Issue operations (get, create, update, delete, transition, comment, link)
3505+
issue Issue operations (get, create, update, delete, transition, comment, link, attach)
34023506
board Board operations (list, get, backlog, issues)
34033507
sprint Sprint operations (list, get, create, start, close, add, remove, report)
34043508
worklog Time tracking (add, list, update, delete, timer)
@@ -3410,6 +3514,9 @@ Commands:
34103514
import Import issues from CSV/JSON file
34113515
search Search with JQL
34123516
me Get current user info
3517+
watch Start watching an issue (receive notifications)
3518+
unwatch Stop watching an issue
3519+
watchers List watchers for an issue
34133520
34143521
Options:
34153522
--profile <name> Use a specific auth profile
@@ -3423,6 +3530,7 @@ Examples:
34233530
atlcli jira analyze velocity --board 123 --sprints 5
34243531
atlcli jira export --jql "project = PROJ" -o issues.json
34253532
atlcli jira import --file issues.json --project PROJ --dry-run
3533+
atlcli jira watch PROJ-123
34263534
atlcli jira search --project PROJ --assignee me
34273535
`;
34283536
}

packages/jira/src/client.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,51 @@ export class JiraClient {
13901390
throw lastError ?? new Error("Attachment upload failed");
13911391
}
13921392

1393+
// ============ Watcher Operations ============
1394+
1395+
/**
1396+
* Get watchers for an issue.
1397+
*
1398+
* GET /rest/api/3/issue/{keyOrId}/watchers
1399+
*/
1400+
async getWatchers(keyOrId: string): Promise<{
1401+
watchers: JiraUser[];
1402+
watchCount: number;
1403+
isWatching: boolean;
1404+
}> {
1405+
const result = await this.request<{
1406+
watchers: JiraUser[];
1407+
watchCount: number;
1408+
isWatching: boolean;
1409+
}>(`/issue/${keyOrId}/watchers`);
1410+
return result;
1411+
}
1412+
1413+
/**
1414+
* Add a watcher to an issue.
1415+
*
1416+
* POST /rest/api/3/issue/{keyOrId}/watchers
1417+
* Body is just the accountId as a quoted string.
1418+
*/
1419+
async addWatcher(keyOrId: string, accountId: string): Promise<void> {
1420+
await this.request(`/issue/${keyOrId}/watchers`, {
1421+
method: "POST",
1422+
body: accountId, // Just the accountId string, not an object
1423+
});
1424+
}
1425+
1426+
/**
1427+
* Remove a watcher from an issue.
1428+
*
1429+
* DELETE /rest/api/3/issue/{keyOrId}/watchers?accountId={accountId}
1430+
*/
1431+
async removeWatcher(keyOrId: string, accountId: string): Promise<void> {
1432+
await this.request(`/issue/${keyOrId}/watchers`, {
1433+
method: "DELETE",
1434+
query: { accountId },
1435+
});
1436+
}
1437+
13931438
// ============ Helpers ============
13941439

13951440
/**

spec/jira-roadmap.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,49 @@ Commercial plugin - not implementing.
320320

321321
---
322322

323+
## 12. Issue Watchers (Priority: Low)
324+
325+
**Status**: COMPLETE ✅
326+
327+
Watch/unwatch issues to receive Jira notifications.
328+
329+
**Features:**
330+
- `jira watch <key>` - Start watching an issue ✅
331+
- `jira unwatch <key>` - Stop watching an issue ✅
332+
- `jira watchers <key>` - List watchers for an issue ✅
333+
334+
**API Endpoints:**
335+
- `GET /rest/api/3/issue/{key}/watchers` - List watchers
336+
- `POST /rest/api/3/issue/{key}/watchers` - Add watcher
337+
- `DELETE /rest/api/3/issue/{key}/watchers?accountId=X` - Remove watcher
338+
339+
---
340+
341+
## 13. Webhook Server (Priority: Low)
342+
343+
**Status**: Not Started
344+
345+
Real-time notifications via local webhook server (like Confluence sync).
346+
347+
**Features:**
348+
- `jira webhook serve [--port 8080]` - Start local webhook server
349+
- `jira webhook register --url <url> --events <events>` - Register webhook with Jira
350+
- `jira webhook list` - List registered webhooks
351+
- `jira webhook delete <id>` - Delete webhook
352+
353+
**Webhook Events:**
354+
- `jira:issue_created`, `jira:issue_updated`, `jira:issue_deleted`
355+
- `comment_created`, `comment_updated`, `comment_deleted`
356+
- `sprint_started`, `sprint_closed`
357+
358+
**Implementation Notes:**
359+
- Use Bun's built-in HTTP server
360+
- Support JQL filtering for targeted notifications
361+
- Output events to stdout or file
362+
- Optional: Desktop notifications via system notify
363+
364+
---
365+
323366
## Priority Order
324367

325368
| Priority | Feature | Effort | Dependencies | Status |
@@ -335,6 +378,8 @@ Commercial plugin - not implementing.
335378
| 9 | Import/Export | Medium | Issues, JQL | ✅ COMPLETE |
336379
| 10 | Saved Filters | Small | JQL | ✅ COMPLETE |
337380
| 11 | Tempo Integration | Medium | Time Tracking | ⏭️ SKIPPED |
381+
| 12 | Issue Watchers | Small | Issues | ✅ COMPLETE |
382+
| 13 | Webhook Server | Medium | Watchers | Not Started |
338383

339384
---
340385

0 commit comments

Comments
 (0)