claude-autopm 2.6.0 → 2.8.1

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.
@@ -0,0 +1,575 @@
1
+ /**
2
+ * Azure DevOps Provider for ClaudeAutoPM
3
+ *
4
+ * Provides bidirectional synchronization with Azure DevOps Work Items
5
+ * following 2025 best practices
6
+ *
7
+ * Features:
8
+ * - Full CRUD operations for work items (Epic, Feature, User Story, Task, Bug)
9
+ * - Comment management
10
+ * - Area Path and Iteration Path management
11
+ * - WIQL queries for advanced filtering
12
+ * - Relation management (Parent/Child links)
13
+ * - State mapping (Azure DevOps <-> Local)
14
+ * - Error handling with meaningful messages
15
+ *
16
+ * @module lib/providers/AzureDevOpsProvider
17
+ */
18
+
19
+ const azdev = require('azure-devops-node-api');
20
+
21
+ /**
22
+ * Azure DevOps Provider Class
23
+ *
24
+ * Manages integration with Azure DevOps Work Item Tracking API
25
+ *
26
+ * @class AzureDevOpsProvider
27
+ */
28
+ class AzureDevOpsProvider {
29
+ /**
30
+ * Creates a new Azure DevOps provider instance
31
+ *
32
+ * @param {Object} options - Configuration options
33
+ * @param {string} [options.token] - Personal Access Token (PAT)
34
+ * @param {string} [options.organization] - Azure DevOps organization name
35
+ * @param {string} [options.project] - Project name
36
+ */
37
+ constructor(options = {}) {
38
+ this.token = options.token || process.env.AZURE_DEVOPS_PAT;
39
+ this.organization = options.organization || process.env.AZURE_DEVOPS_ORG;
40
+ this.project = options.project || process.env.AZURE_DEVOPS_PROJECT;
41
+
42
+ this.connection = null;
43
+ this.witApi = null;
44
+ }
45
+
46
+ /**
47
+ * Authenticates with Azure DevOps API
48
+ *
49
+ * Creates connection using PAT and verifies project access
50
+ *
51
+ * @async
52
+ * @returns {Promise<Object>} Project object
53
+ * @throws {Error} If token, organization, or project is missing
54
+ * @throws {Error} If project not found or access denied
55
+ */
56
+ async authenticate() {
57
+ if (!this.token) {
58
+ throw new Error('Azure DevOps PAT token is required');
59
+ }
60
+
61
+ if (!this.organization) {
62
+ throw new Error('Azure DevOps organization is required');
63
+ }
64
+
65
+ if (!this.project) {
66
+ throw new Error('Azure DevOps project is required');
67
+ }
68
+
69
+ // Create organization URL
70
+ const orgUrl = `https://dev.azure.com/${this.organization}`;
71
+
72
+ // Create auth handler
73
+ const authHandler = azdev.getPersonalAccessTokenHandler(this.token);
74
+
75
+ // Create connection
76
+ this.connection = new azdev.WebApi(orgUrl, authHandler);
77
+
78
+ // Get Work Item Tracking API
79
+ this.witApi = await this.connection.getWorkItemTrackingApi();
80
+
81
+ // Verify project access
82
+ const project = await this.witApi.getProject(this.project);
83
+ if (!project) {
84
+ throw new Error('Project not found or access denied');
85
+ }
86
+
87
+ return project;
88
+ }
89
+
90
+ /**
91
+ * Fetches a single work item by ID
92
+ *
93
+ * @async
94
+ * @param {number} id - Work item ID
95
+ * @param {string} [expand] - Expand option (None, Relations, Fields, Links, All)
96
+ * @returns {Promise<Object>} Work item object
97
+ * @throws {Error} If work item is not found
98
+ */
99
+ async getWorkItem(id, expand) {
100
+ try {
101
+ return await this._makeRequest(async () => {
102
+ return await this.witApi.getWorkItem(id, expand);
103
+ });
104
+ } catch (error) {
105
+ if (error.statusCode === 404) {
106
+ throw new Error(`Work item not found: ${id}`);
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Lists work items with optional filtering
114
+ *
115
+ * @async
116
+ * @param {Object} [filters={}] - Filter options
117
+ * @param {string} [filters.type] - Work item type (Epic, Feature, User Story, Task, Bug)
118
+ * @param {string} [filters.state] - Work item state (New, Active, Resolved, Closed)
119
+ * @param {string} [filters.areaPath] - Area path filter
120
+ * @param {string} [filters.iterationPath] - Iteration path filter
121
+ * @returns {Promise<Array>} Array of work item objects
122
+ */
123
+ async listWorkItems(filters = {}) {
124
+ // Build WIQL query
125
+ let wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '${this.project}'`;
126
+
127
+ if (filters.type) {
128
+ wiql += ` AND [System.WorkItemType] = '${filters.type}'`;
129
+ }
130
+
131
+ if (filters.state) {
132
+ wiql += ` AND [System.State] = '${filters.state}'`;
133
+ }
134
+
135
+ if (filters.areaPath) {
136
+ wiql += ` AND [System.AreaPath] = '${filters.areaPath}'`;
137
+ }
138
+
139
+ if (filters.iterationPath) {
140
+ wiql += ` AND [System.IterationPath] = '${filters.iterationPath}'`;
141
+ }
142
+
143
+ // Execute query
144
+ const queryResult = await this.witApi.queryByWiql(
145
+ { query: wiql },
146
+ this.project
147
+ );
148
+
149
+ // Get work item IDs
150
+ const ids = queryResult.workItems ? queryResult.workItems.map(wi => wi.id) : [];
151
+
152
+ if (ids.length === 0) {
153
+ return [];
154
+ }
155
+
156
+ // Fetch full work items
157
+ const workItems = await this.witApi.getWorkItems(
158
+ ids,
159
+ undefined,
160
+ undefined,
161
+ 'None'
162
+ );
163
+
164
+ return workItems;
165
+ }
166
+
167
+ /**
168
+ * Creates a new work item
169
+ *
170
+ * @async
171
+ * @param {string} type - Work item type (Epic, Feature, User Story, Task, Bug)
172
+ * @param {Object} data - Work item data
173
+ * @param {string} data.title - Work item title (required)
174
+ * @param {string} [data.description] - Work item description
175
+ * @param {string} [data.state] - Work item state
176
+ * @param {string} [data.areaPath] - Area path
177
+ * @param {string} [data.iterationPath] - Iteration path
178
+ * @returns {Promise<Object>} Created work item object
179
+ * @throws {Error} If title is missing
180
+ */
181
+ async createWorkItem(type, data) {
182
+ if (!data.title) {
183
+ throw new Error('Work item title is required');
184
+ }
185
+
186
+ // Build JSON Patch Document
187
+ const patchDoc = [
188
+ {
189
+ op: 'add',
190
+ path: '/fields/System.Title',
191
+ value: data.title
192
+ }
193
+ ];
194
+
195
+ if (data.description) {
196
+ patchDoc.push({
197
+ op: 'add',
198
+ path: '/fields/System.Description',
199
+ value: data.description
200
+ });
201
+ }
202
+
203
+ if (data.state) {
204
+ patchDoc.push({
205
+ op: 'add',
206
+ path: '/fields/System.State',
207
+ value: data.state
208
+ });
209
+ }
210
+
211
+ if (data.areaPath) {
212
+ patchDoc.push({
213
+ op: 'add',
214
+ path: '/fields/System.AreaPath',
215
+ value: data.areaPath
216
+ });
217
+ }
218
+
219
+ if (data.iterationPath) {
220
+ patchDoc.push({
221
+ op: 'add',
222
+ path: '/fields/System.IterationPath',
223
+ value: data.iterationPath
224
+ });
225
+ }
226
+
227
+ return await this.witApi.createWorkItem(
228
+ null,
229
+ patchDoc,
230
+ this.project,
231
+ type
232
+ );
233
+ }
234
+
235
+ /**
236
+ * Updates an existing work item
237
+ *
238
+ * @async
239
+ * @param {number} id - Work item ID
240
+ * @param {Object} data - Fields to update
241
+ * @param {string} [data.title] - New title
242
+ * @param {string} [data.description] - New description
243
+ * @param {string} [data.state] - New state
244
+ * @param {string} [data.areaPath] - New area path
245
+ * @param {string} [data.iterationPath] - New iteration path
246
+ * @returns {Promise<Object>} Updated work item object
247
+ */
248
+ async updateWorkItem(id, data) {
249
+ // Build JSON Patch Document
250
+ const patchDoc = [];
251
+
252
+ if (data.title) {
253
+ patchDoc.push({
254
+ op: 'replace',
255
+ path: '/fields/System.Title',
256
+ value: data.title
257
+ });
258
+ }
259
+
260
+ if (data.description) {
261
+ patchDoc.push({
262
+ op: 'replace',
263
+ path: '/fields/System.Description',
264
+ value: data.description
265
+ });
266
+ }
267
+
268
+ if (data.state) {
269
+ patchDoc.push({
270
+ op: 'replace',
271
+ path: '/fields/System.State',
272
+ value: data.state
273
+ });
274
+ }
275
+
276
+ if (data.areaPath) {
277
+ patchDoc.push({
278
+ op: 'replace',
279
+ path: '/fields/System.AreaPath',
280
+ value: data.areaPath
281
+ });
282
+ }
283
+
284
+ if (data.iterationPath) {
285
+ patchDoc.push({
286
+ op: 'replace',
287
+ path: '/fields/System.IterationPath',
288
+ value: data.iterationPath
289
+ });
290
+ }
291
+
292
+ return await this.witApi.updateWorkItem(null, patchDoc, id);
293
+ }
294
+
295
+ /**
296
+ * Deletes a work item
297
+ *
298
+ * @async
299
+ * @param {number} id - Work item ID
300
+ * @returns {Promise<Object>} Delete result
301
+ * @throws {Error} If work item not found
302
+ */
303
+ async deleteWorkItem(id) {
304
+ try {
305
+ return await this._makeRequest(async () => {
306
+ return await this.witApi.deleteWorkItem(id);
307
+ }, { id });
308
+ } catch (error) {
309
+ if (error.statusCode === 404) {
310
+ throw new Error(`Work item not found: ${id}`);
311
+ }
312
+ throw error;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Adds a comment to a work item
318
+ *
319
+ * @async
320
+ * @param {number} workItemId - Work item ID
321
+ * @param {string} text - Comment text
322
+ * @returns {Promise<Object>} Created comment object
323
+ * @throws {Error} If text is empty
324
+ */
325
+ async addComment(workItemId, text) {
326
+ if (!text || text.trim() === '') {
327
+ throw new Error('Comment text is required');
328
+ }
329
+
330
+ return await this.witApi.addComment(
331
+ { text },
332
+ this.project,
333
+ workItemId
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Gets all comments for a work item
339
+ *
340
+ * @async
341
+ * @param {number} workItemId - Work item ID
342
+ * @returns {Promise<Object>} Comments object with comments array
343
+ */
344
+ async getComments(workItemId) {
345
+ return await this.witApi.getComments(this.project, workItemId);
346
+ }
347
+
348
+ /**
349
+ * Updates a comment
350
+ *
351
+ * @async
352
+ * @param {number} workItemId - Work item ID
353
+ * @param {number} commentId - Comment ID
354
+ * @param {string} text - New comment text
355
+ * @returns {Promise<Object>} Updated comment object
356
+ */
357
+ async updateComment(workItemId, commentId, text) {
358
+ return await this.witApi.updateComment(
359
+ { text },
360
+ this.project,
361
+ workItemId,
362
+ commentId
363
+ );
364
+ }
365
+
366
+ /**
367
+ * Deletes a comment
368
+ *
369
+ * @async
370
+ * @param {number} workItemId - Work item ID
371
+ * @param {number} commentId - Comment ID
372
+ * @returns {Promise<void>}
373
+ */
374
+ async deleteComment(workItemId, commentId) {
375
+ return await this.witApi.deleteComment(
376
+ this.project,
377
+ workItemId,
378
+ commentId
379
+ );
380
+ }
381
+
382
+ /**
383
+ * Sets the area path for a work item
384
+ *
385
+ * @async
386
+ * @param {number} workItemId - Work item ID
387
+ * @param {string} path - Area path (e.g., "project\\Team A")
388
+ * @returns {Promise<Object>} Updated work item object
389
+ */
390
+ async setAreaPath(workItemId, path) {
391
+ const patchDoc = [
392
+ {
393
+ op: 'replace',
394
+ path: '/fields/System.AreaPath',
395
+ value: path
396
+ }
397
+ ];
398
+
399
+ return await this.witApi.updateWorkItem(null, patchDoc, workItemId);
400
+ }
401
+
402
+ /**
403
+ * Sets the iteration path for a work item
404
+ *
405
+ * @async
406
+ * @param {number} workItemId - Work item ID
407
+ * @param {string} path - Iteration path (e.g., "project\\Sprint 1")
408
+ * @returns {Promise<Object>} Updated work item object
409
+ */
410
+ async setIterationPath(workItemId, path) {
411
+ const patchDoc = [
412
+ {
413
+ op: 'replace',
414
+ path: '/fields/System.IterationPath',
415
+ value: path
416
+ }
417
+ ];
418
+
419
+ return await this.witApi.updateWorkItem(null, patchDoc, workItemId);
420
+ }
421
+
422
+ /**
423
+ * Executes a WIQL query
424
+ *
425
+ * @async
426
+ * @param {string} wiql - WIQL query string
427
+ * @param {Object} [options={}] - Query options
428
+ * @param {string} [options.expand] - Expand option for work items
429
+ * @returns {Promise<Array>} Array of work item objects
430
+ */
431
+ async queryWorkItems(wiql, options = {}) {
432
+ // Execute query
433
+ const queryResult = await this.witApi.queryByWiql(
434
+ { query: wiql },
435
+ this.project
436
+ );
437
+
438
+ // Get work item IDs
439
+ const ids = queryResult.workItems ? queryResult.workItems.map(wi => wi.id) : [];
440
+
441
+ if (ids.length === 0 || !ids.length) {
442
+ return [];
443
+ }
444
+
445
+ // Fetch full work items
446
+ const expand = options.expand || 'None';
447
+ const workItems = await this.witApi.getWorkItems(
448
+ ids,
449
+ undefined,
450
+ undefined,
451
+ expand
452
+ );
453
+
454
+ return workItems;
455
+ }
456
+
457
+ /**
458
+ * Adds a relation to a work item
459
+ *
460
+ * @async
461
+ * @param {number} workItemId - Work item ID
462
+ * @param {Object} relation - Relation object
463
+ * @param {string} relation.rel - Relation type (e.g., "System.LinkTypes.Hierarchy-Reverse")
464
+ * @param {string} relation.url - Related work item URL
465
+ * @returns {Promise<Object>} Updated work item object
466
+ */
467
+ async addRelation(workItemId, relation) {
468
+ const patchDoc = [
469
+ {
470
+ op: 'add',
471
+ path: '/relations/-',
472
+ value: relation
473
+ }
474
+ ];
475
+
476
+ return await this.witApi.updateWorkItem(null, patchDoc, workItemId);
477
+ }
478
+
479
+ /**
480
+ * Gets all relations for a work item
481
+ *
482
+ * @async
483
+ * @param {number} workItemId - Work item ID
484
+ * @returns {Promise<Array>} Array of relation objects
485
+ */
486
+ async getRelations(workItemId) {
487
+ const workItem = await this.witApi.getWorkItem(workItemId, 'Relations');
488
+
489
+ return workItem.relations || [];
490
+ }
491
+
492
+ /**
493
+ * Checks rate limit status
494
+ *
495
+ * Azure DevOps does not expose rate limit information via API
496
+ *
497
+ * @async
498
+ * @returns {Promise<Object>} Rate limit information (not available)
499
+ */
500
+ async checkRateLimit() {
501
+ return {
502
+ available: false,
503
+ message: 'Azure DevOps does not expose rate limit information via API'
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Maps Azure DevOps state to local status
509
+ *
510
+ * @param {string} azureState - Azure DevOps state
511
+ * @returns {string} Local status
512
+ */
513
+ _mapStateToLocal(azureState) {
514
+ const stateMap = {
515
+ 'New': 'open',
516
+ 'Active': 'in-progress',
517
+ 'Resolved': 'done',
518
+ 'Closed': 'closed',
519
+ 'Removed': 'closed'
520
+ };
521
+
522
+ return stateMap[azureState] || azureState;
523
+ }
524
+
525
+ /**
526
+ * Maps local status to Azure DevOps state
527
+ *
528
+ * @param {string} localStatus - Local status
529
+ * @returns {string} Azure DevOps state
530
+ */
531
+ _mapLocalStatusToState(localStatus) {
532
+ const statusMap = {
533
+ 'open': 'New',
534
+ 'in-progress': 'Active',
535
+ 'done': 'Resolved',
536
+ 'closed': 'Closed'
537
+ };
538
+
539
+ return statusMap[localStatus] || localStatus;
540
+ }
541
+
542
+ /**
543
+ * Makes a request with error handling
544
+ *
545
+ * @async
546
+ * @param {Function} requestFn - Function that makes the request
547
+ * @param {Object} [context={}] - Context for error messages
548
+ * @returns {Promise<*>} Response data
549
+ * @throws {Error} If request fails with meaningful error message
550
+ * @private
551
+ */
552
+ async _makeRequest(requestFn, context = {}) {
553
+ try {
554
+ return await requestFn();
555
+ } catch (error) {
556
+ // Handle specific error codes
557
+ if (error.statusCode === 401) {
558
+ throw new Error('Authentication failed - check AZURE_DEVOPS_PAT');
559
+ }
560
+
561
+ if (error.statusCode === 403) {
562
+ throw new Error('Access denied - check PAT permissions');
563
+ }
564
+
565
+ if (error.statusCode === 404 && context.id) {
566
+ throw new Error(`Work item not found: ${context.id}`);
567
+ }
568
+
569
+ // Rethrow other errors
570
+ throw error;
571
+ }
572
+ }
573
+ }
574
+
575
+ module.exports = AzureDevOpsProvider;