@falldownthesystem/devtool 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falldownthesystem/devtool",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for Bitbucket PRs, Jira tickets, Confluence pages, and Service Desk tickets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@ function authHeader() {
10
10
  return 'Basic ' + Buffer.from(`${email}:${token}`).toString('base64');
11
11
  }
12
12
 
13
- async function request(path, params = {}) {
13
+ async function request(path, params = {}, extraHeaders = {}) {
14
14
  const url = new URL(path.startsWith('http') ? path : `${baseUrl()}${path}`);
15
15
  for (const [k, v] of Object.entries(params)) {
16
16
  if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v);
@@ -20,6 +20,7 @@ async function request(path, params = {}) {
20
20
  headers: {
21
21
  'Authorization': authHeader(),
22
22
  'Accept': 'application/json',
23
+ ...extraHeaders,
23
24
  },
24
25
  });
25
26
 
@@ -70,3 +71,49 @@ export async function getMyIssues() {
70
71
  : `assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC`;
71
72
  return searchIssues(jql);
72
73
  }
74
+
75
+ let _serviceDeskIdCache = null;
76
+
77
+ async function getServiceDeskId(projectKey) {
78
+ if (_serviceDeskIdCache && _serviceDeskIdCache.projectKey === projectKey) {
79
+ return _serviceDeskIdCache.id;
80
+ }
81
+ const data = await request('/rest/servicedeskapi/servicedesk', { start: 0, limit: 100 }, { 'X-ExperimentalApi': 'opt-in' });
82
+ const match = (data.values || []).find((sd) => sd.projectKey === projectKey);
83
+ if (!match) {
84
+ throw new Error(`No Service Desk found for project ${projectKey}`);
85
+ }
86
+ _serviceDeskIdCache = { projectKey, id: match.id };
87
+ return match.id;
88
+ }
89
+
90
+ export async function listQueues() {
91
+ const projectKey = get('serviceDesk.defaultProject');
92
+ if (!projectKey) throw new Error('serviceDesk.defaultProject is not configured');
93
+ const sdId = await getServiceDeskId(projectKey);
94
+ const data = await request(`/rest/servicedeskapi/servicedesk/${sdId}/queue`, { start: 0, limit: 100 }, { 'X-ExperimentalApi': 'opt-in' });
95
+ return (data.values || []).map((q) => ({ id: q.id, name: q.name, jql: q.jql }));
96
+ }
97
+
98
+ export async function findQueue(nameOrId) {
99
+ const queues = await listQueues();
100
+ const idMatch = queues.find((q) => String(q.id) === String(nameOrId));
101
+ if (idMatch) return idMatch;
102
+ const lower = nameOrId.toLowerCase();
103
+ const exact = queues.find((q) => q.name.toLowerCase() === lower);
104
+ if (exact) return exact;
105
+ const partial = queues.filter((q) => q.name.toLowerCase().includes(lower));
106
+ if (partial.length === 1) return partial[0];
107
+ if (partial.length > 1) {
108
+ const err = new Error(`Ambiguous queue "${nameOrId}" — matches: ${partial.map((q) => q.name).join(', ')}`);
109
+ err.candidates = partial;
110
+ throw err;
111
+ }
112
+ throw new Error(`No queue found matching "${nameOrId}"`);
113
+ }
114
+
115
+ export async function searchByQueue(nameOrId, maxResults = 50, nextPageToken = '') {
116
+ const queue = await findQueue(nameOrId);
117
+ const result = await searchIssues(queue.jql, maxResults, nextPageToken);
118
+ return { ...result, queue };
119
+ }
@@ -4,11 +4,14 @@ import {
4
4
  getIssueComments,
5
5
  searchIssues,
6
6
  getMyIssues,
7
+ listQueues,
8
+ searchByQueue,
7
9
  } from '../clients/serviceDesk.js';
8
10
  import {
9
11
  formatIssueView, formatIssueComments, formatIssueSearch,
10
12
  plainIssueView, plainIssueComments, plainIssueSearch,
11
13
  curateIssueJson, curateCommentsJson, curateSearchJson,
14
+ formatQueueList, plainQueueList, curateQueuesJson,
12
15
  } from '../formatters/serviceDesk.js';
13
16
  import { spinner } from '../utils/output.js';
14
17
  import { handleError } from '../utils/errors.js';
@@ -74,6 +77,43 @@ sd.command('search <jql>')
74
77
  }
75
78
  });
76
79
 
80
+ sd.command('queues')
81
+ .description('List Service Desk queues for the default project')
82
+ .action(async function () {
83
+ const mode = getOutputMode(this);
84
+ const s = mode === 'rich' ? spinner('Fetching queues...').start() : null;
85
+ try {
86
+ const queues = await listQueues();
87
+ if (s) s.success({ text: `${queues.length} queue(s)` });
88
+ if (mode === 'json') outputJson(curateQueuesJson(queues));
89
+ else if (mode === 'plain') plainQueueList(queues);
90
+ else formatQueueList(queues);
91
+ } catch (err) {
92
+ if (s) s.error({ text: 'Failed to fetch queues' });
93
+ handleError(err, 'sd queues');
94
+ }
95
+ });
96
+
97
+ sd.command('queue <nameOrId>')
98
+ .description('Show issues in a Service Desk queue (by name or id)')
99
+ .option('-n, --per-page <number>', 'Results per page', '20')
100
+ .option('--next <token>', 'Continuation token from a previous page')
101
+ .action(async function (nameOrId, opts) {
102
+ const mode = getOutputMode(this);
103
+ const perPage = parseInt(opts.perPage, 10);
104
+ const s = mode === 'rich' ? spinner(`Fetching queue "${nameOrId}"...`).start() : null;
105
+ try {
106
+ const result = await searchByQueue(nameOrId, perPage, opts.next || '');
107
+ if (s) s.success({ text: `${result.issues.length} issue(s) in "${result.queue.name}"` });
108
+ if (mode === 'json') outputJson(curateSearchJson(result));
109
+ else if (mode === 'plain') plainIssueSearch(result);
110
+ else formatIssueSearch(result);
111
+ } catch (err) {
112
+ if (s) s.error({ text: `Failed to fetch queue "${nameOrId}"` });
113
+ handleError(err, `sd queue ${nameOrId}`);
114
+ }
115
+ });
116
+
77
117
  sd.command('mine')
78
118
  .alias('my')
79
119
  .description('Show my open Service Desk tickets')
@@ -59,6 +59,10 @@ function requestType(fields) {
59
59
  return null;
60
60
  }
61
61
 
62
+ function migrationServerKey(fields) {
63
+ return fields.customfield_10165 || null;
64
+ }
65
+
62
66
  // --- JSON curators ---
63
67
 
64
68
  export function curateIssueJson(issue) {
@@ -70,6 +74,7 @@ export function curateIssueJson(issue) {
70
74
  priority: f.priority?.name || null,
71
75
  type: f.issuetype?.name || null,
72
76
  requestType: requestType(f),
77
+ migrationServerKey: migrationServerKey(f),
73
78
  assignee: f.assignee?.displayName || null,
74
79
  reporter: f.reporter?.displayName || null,
75
80
  labels: f.labels || [],
@@ -92,6 +97,7 @@ export function curateSearchJson(result) {
92
97
  return {
93
98
  nextPageToken: result.nextPageToken,
94
99
  isLast: result.isLast,
100
+ queue: result.queue ? { id: result.queue.id, name: result.queue.name, jql: result.queue.jql } : undefined,
95
101
  issues: result.issues.map((issue) => ({
96
102
  key: issue.key,
97
103
  summary: issue.fields.summary,
@@ -103,6 +109,31 @@ export function curateSearchJson(result) {
103
109
  };
104
110
  }
105
111
 
112
+ export function curateQueuesJson(queues) {
113
+ return queues.map((q) => ({ id: q.id, name: q.name, jql: q.jql }));
114
+ }
115
+
116
+ export function plainQueueList(queues) {
117
+ if (queues.length === 0) { console.log('No queues.'); return; }
118
+ for (const q of queues) {
119
+ console.log(`${q.id}\t${q.name}\t${q.jql}`);
120
+ }
121
+ }
122
+
123
+ export function formatQueueList(queues) {
124
+ if (queues.length === 0) {
125
+ console.log(pc.dim(' No queues.'));
126
+ return;
127
+ }
128
+ const rows = queues.map((q) => [
129
+ pc.dim(String(q.id)),
130
+ pc.bold(q.name),
131
+ (q.jql || '').length > 80 ? q.jql.slice(0, 77) + '...' : q.jql || '',
132
+ ]);
133
+ console.log(table(['ID', 'Name', 'JQL'], rows));
134
+ console.log('');
135
+ }
136
+
106
137
  // --- Plain formatters ---
107
138
 
108
139
  export function plainIssueView(issue) {
@@ -113,6 +144,8 @@ export function plainIssueView(issue) {
113
144
  console.log(`Type: ${f.issuetype?.name || 'Unknown'}`);
114
145
  const rt = requestType(f);
115
146
  if (rt) console.log(`Request Type: ${rt}`);
147
+ const mk = migrationServerKey(f);
148
+ if (mk) console.log(`Migration Server Key: ${mk}`);
116
149
  console.log(`Assignee: ${f.assignee?.displayName || 'Unassigned'}`);
117
150
  console.log(`Reporter: ${f.reporter?.displayName || 'Unknown'}`);
118
151
  if (f.labels?.length > 0) console.log(`Labels: ${f.labels.join(', ')}`);
@@ -131,6 +164,10 @@ export function plainIssueComments(comments) {
131
164
  }
132
165
 
133
166
  export function plainIssueSearch(result) {
167
+ if (result.queue) {
168
+ console.log(`Queue: ${result.queue.name} (${result.queue.id})`);
169
+ console.log(`JQL: ${result.queue.jql}`);
170
+ }
134
171
  if (result.issues.length === 0) { console.log('No issues found.'); return; }
135
172
  for (const issue of result.issues) {
136
173
  const f = issue.fields;
@@ -153,6 +190,8 @@ export function formatIssueView(issue) {
153
190
  field('Type', f.issuetype?.name);
154
191
  const rt = requestType(f);
155
192
  if (rt) field('Request Type', rt);
193
+ const mk = migrationServerKey(f);
194
+ if (mk) field('Migration Server Key', mk);
156
195
  field('Assignee', f.assignee?.displayName || pc.dim('Unassigned'));
157
196
  field('Reporter', f.reporter?.displayName);
158
197
  field('Labels', f.labels?.length > 0 ? f.labels.join(', ') : null);
@@ -197,6 +236,12 @@ export function formatIssueComments(comments) {
197
236
  export function formatIssueSearch(result) {
198
237
  const { issues } = result;
199
238
 
239
+ if (result.queue) {
240
+ heading(`Queue: ${result.queue.name}`);
241
+ console.log(pc.dim(` ${result.queue.jql}`));
242
+ divider();
243
+ }
244
+
200
245
  if (issues.length === 0) {
201
246
  console.log(pc.dim(' No issues found.'));
202
247
  return;
package/src/index.js CHANGED
@@ -13,7 +13,7 @@ const program = new Command();
13
13
  program
14
14
  .name('dev')
15
15
  .description('CLI for Bitbucket PRs, Jira tickets, Confluence pages, and Service Desk tickets')
16
- .version('0.3.0')
16
+ .version('0.4.0')
17
17
  .option('--json', 'Output raw JSON')
18
18
  .option('--plain', 'Output compact plain text (no tables, no colors)');
19
19