@mcp-consultant-tools/azure-devops 27.0.0 → 28.0.0-beta.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.
Files changed (109) hide show
  1. package/build/azure-devops-client.d.ts +42 -0
  2. package/build/azure-devops-client.d.ts.map +1 -0
  3. package/build/azure-devops-client.js +144 -0
  4. package/build/azure-devops-client.js.map +1 -0
  5. package/build/index.d.ts +8 -11
  6. package/build/index.d.ts.map +1 -1
  7. package/build/index.js +40 -1878
  8. package/build/index.js.map +1 -1
  9. package/build/models/api-types.d.ts +159 -0
  10. package/build/models/api-types.d.ts.map +1 -0
  11. package/build/models/api-types.js +5 -0
  12. package/build/models/api-types.js.map +1 -0
  13. package/build/models/index.d.ts +5 -0
  14. package/build/models/index.d.ts.map +1 -0
  15. package/build/models/index.js +5 -0
  16. package/build/models/index.js.map +1 -0
  17. package/build/prompts/index.d.ts +3 -0
  18. package/build/prompts/index.d.ts.map +1 -0
  19. package/build/prompts/index.js +83 -0
  20. package/build/prompts/index.js.map +1 -0
  21. package/build/prompts/templates.d.ts +8 -0
  22. package/build/prompts/templates.d.ts.map +1 -0
  23. package/build/prompts/templates.js +126 -0
  24. package/build/prompts/templates.js.map +1 -0
  25. package/build/services/build-service.d.ts +16 -0
  26. package/build/services/build-service.d.ts.map +1 -0
  27. package/build/services/build-service.js +195 -0
  28. package/build/services/build-service.js.map +1 -0
  29. package/build/services/configuration-service.d.ts +7 -0
  30. package/build/services/configuration-service.d.ts.map +1 -0
  31. package/build/services/configuration-service.js +24 -0
  32. package/build/services/configuration-service.js.map +1 -0
  33. package/build/services/index.d.ts +11 -0
  34. package/build/services/index.d.ts.map +1 -0
  35. package/build/services/index.js +11 -0
  36. package/build/services/index.js.map +1 -0
  37. package/build/services/pull-request-service.d.ts +29 -0
  38. package/build/services/pull-request-service.d.ts.map +1 -0
  39. package/build/services/pull-request-service.js +390 -0
  40. package/build/services/pull-request-service.js.map +1 -0
  41. package/build/services/sync-service.d.ts +19 -0
  42. package/build/services/sync-service.d.ts.map +1 -0
  43. package/build/services/sync-service.js +439 -0
  44. package/build/services/sync-service.js.map +1 -0
  45. package/build/services/variable-group-service.d.ts +11 -0
  46. package/build/services/variable-group-service.d.ts.map +1 -0
  47. package/build/services/variable-group-service.js +62 -0
  48. package/build/services/variable-group-service.js.map +1 -0
  49. package/build/services/wiki-service.d.ts +41 -0
  50. package/build/services/wiki-service.d.ts.map +1 -0
  51. package/build/services/wiki-service.js +346 -0
  52. package/build/services/wiki-service.js.map +1 -0
  53. package/build/services/work-item-service.d.ts +22 -0
  54. package/build/services/work-item-service.d.ts.map +1 -0
  55. package/build/services/work-item-service.js +284 -0
  56. package/build/services/work-item-service.js.map +1 -0
  57. package/build/sync/file-utils.d.ts +7 -5
  58. package/build/sync/file-utils.d.ts.map +1 -1
  59. package/build/sync/file-utils.js +17 -8
  60. package/build/sync/file-utils.js.map +1 -1
  61. package/build/sync/markdown-serializer.d.ts +5 -4
  62. package/build/sync/markdown-serializer.d.ts.map +1 -1
  63. package/build/sync/markdown-serializer.js +21 -9
  64. package/build/sync/markdown-serializer.js.map +1 -1
  65. package/build/tool-examples.d.ts +8 -0
  66. package/build/tool-examples.d.ts.map +1 -1
  67. package/build/tool-examples.js +28 -0
  68. package/build/tool-examples.js.map +1 -1
  69. package/build/tools/build-tools.d.ts +3 -0
  70. package/build/tools/build-tools.d.ts.map +1 -0
  71. package/build/tools/build-tools.js +56 -0
  72. package/build/tools/build-tools.js.map +1 -0
  73. package/build/tools/configuration-tools.d.ts +6 -0
  74. package/build/tools/configuration-tools.d.ts.map +1 -0
  75. package/build/tools/configuration-tools.js +30 -0
  76. package/build/tools/configuration-tools.js.map +1 -0
  77. package/build/tools/index.d.ts +13 -0
  78. package/build/tools/index.d.ts.map +1 -0
  79. package/build/tools/index.js +30 -0
  80. package/build/tools/index.js.map +1 -0
  81. package/build/tools/pull-request-tools.d.ts +5 -0
  82. package/build/tools/pull-request-tools.d.ts.map +1 -0
  83. package/build/tools/pull-request-tools.js +227 -0
  84. package/build/tools/pull-request-tools.js.map +1 -0
  85. package/build/tools/sync-tools.d.ts +3 -0
  86. package/build/tools/sync-tools.d.ts.map +1 -0
  87. package/build/tools/sync-tools.js +146 -0
  88. package/build/tools/sync-tools.js.map +1 -0
  89. package/build/tools/variable-group-tools.d.ts +3 -0
  90. package/build/tools/variable-group-tools.d.ts.map +1 -0
  91. package/build/tools/variable-group-tools.js +32 -0
  92. package/build/tools/variable-group-tools.js.map +1 -0
  93. package/build/tools/wiki-tools.d.ts +3 -0
  94. package/build/tools/wiki-tools.d.ts.map +1 -0
  95. package/build/tools/wiki-tools.js +151 -0
  96. package/build/tools/wiki-tools.js.map +1 -0
  97. package/build/tools/work-item-tools.d.ts +3 -0
  98. package/build/tools/work-item-tools.d.ts.map +1 -0
  99. package/build/tools/work-item-tools.js +188 -0
  100. package/build/tools/work-item-tools.js.map +1 -0
  101. package/build/types.d.ts +23 -0
  102. package/build/types.d.ts.map +1 -0
  103. package/build/types.js +2 -0
  104. package/build/types.js.map +1 -0
  105. package/package.json +2 -2
  106. package/build/AzureDevOpsService.d.ts +0 -489
  107. package/build/AzureDevOpsService.d.ts.map +0 -1
  108. package/build/AzureDevOpsService.js +0 -1558
  109. package/build/AzureDevOpsService.js.map +0 -1
@@ -1,1558 +0,0 @@
1
- import axios from 'axios';
2
- import { marked } from 'marked';
3
- import { getAllLargeTextFields } from './sync/html-detection.js';
4
- export class AzureDevOpsService {
5
- config;
6
- baseUrl;
7
- searchUrl;
8
- authHeader;
9
- apiVersion;
10
- constructor(config) {
11
- this.config = {
12
- ...config,
13
- apiVersion: config.apiVersion || '7.1',
14
- enableWorkItemWrite: config.enableWorkItemWrite ?? false,
15
- enableWorkItemDelete: config.enableWorkItemDelete ?? false,
16
- enableWikiWrite: config.enableWikiWrite ?? false,
17
- enableWikiDelete: config.enableWikiDelete ?? false,
18
- enablePullRequestWrite: config.enablePullRequestWrite ?? false,
19
- };
20
- this.baseUrl = `https://dev.azure.com/${this.config.organization}`;
21
- this.searchUrl = `https://almsearch.dev.azure.com/${this.config.organization}`;
22
- this.apiVersion = this.config.apiVersion;
23
- // Encode PAT for Basic Auth (format is :PAT encoded in base64)
24
- this.authHeader = `Basic ${Buffer.from(`:${this.config.pat}`).toString('base64')}`;
25
- }
26
- /**
27
- * Validate that a project is in the allowed list
28
- */
29
- validateProject(project) {
30
- if (!this.config.projects.includes(project)) {
31
- throw new Error(`Project '${project}' is not in the allowed projects list. Allowed projects: ${this.config.projects.join(', ')}`);
32
- }
33
- }
34
- /**
35
- * Make an authenticated request to the Azure DevOps API
36
- */
37
- async makeRequest(endpoint, method = 'GET', data, useSearchUrl = false, customHeaders) {
38
- try {
39
- const baseUrl = useSearchUrl ? this.searchUrl : this.baseUrl;
40
- const url = `${baseUrl}/${endpoint}`;
41
- const response = await axios({
42
- method,
43
- url,
44
- headers: {
45
- 'Authorization': this.authHeader,
46
- 'Content-Type': method === 'PATCH' ? 'application/json-patch+json' : 'application/json',
47
- 'Accept': 'application/json',
48
- ...customHeaders // Merge custom headers (can override defaults)
49
- },
50
- data
51
- });
52
- return response.data;
53
- }
54
- catch (error) {
55
- const errorDetails = error.response?.data?.message || error.response?.data || error.message;
56
- console.error('Azure DevOps API request failed:', {
57
- endpoint,
58
- method,
59
- status: error.response?.status,
60
- statusText: error.response?.statusText,
61
- error: errorDetails
62
- });
63
- // Provide user-friendly error messages
64
- if (error.response?.status === 401) {
65
- throw new Error('Azure DevOps authentication failed. Please check your PAT token and permissions.');
66
- }
67
- if (error.response?.status === 403) {
68
- throw new Error('Azure DevOps access denied. Please check your PAT scopes and project permissions.');
69
- }
70
- if (error.response?.status === 404) {
71
- throw new Error(`Azure DevOps resource not found: ${endpoint}`);
72
- }
73
- throw new Error(`Azure DevOps API request failed: ${error.message} - ${JSON.stringify(errorDetails)}`);
74
- }
75
- }
76
- // ==================== WIKI OPERATIONS ====================
77
- /**
78
- * Convert a git path (returned by search) to a wiki path (used by get-page API)
79
- * Git paths use dashes and .md extensions: /Release-Notes/Page-Name.md
80
- * Wiki paths use spaces and no extensions: /Release Notes/Page Name
81
- * @param gitPath The git path from search results
82
- * @returns The wiki path for use with get-page API
83
- */
84
- convertGitPathToWikiPath(gitPath) {
85
- return gitPath
86
- .replace(/\.md$/, '') // Remove .md extension
87
- .replace(/-/g, ' ') // Replace ALL dashes with spaces
88
- .replace(/%2D/gi, '-'); // Decode %2D back to - (actual dashes in page names)
89
- }
90
- /**
91
- * Count occurrences of a string in content
92
- * @param content The content to search in
93
- * @param searchStr The string to search for
94
- * @returns Number of occurrences
95
- */
96
- countOccurrences(content, searchStr) {
97
- const regex = new RegExp(this.escapeRegExp(searchStr), 'g');
98
- const matches = content.match(regex);
99
- return matches ? matches.length : 0;
100
- }
101
- /**
102
- * Get locations where a string appears in content
103
- * @param content The content to search in
104
- * @param searchStr The string to search for
105
- * @returns Formatted string showing line numbers and context
106
- */
107
- getMatchLocations(content, searchStr) {
108
- const lines = content.split('\n');
109
- const matches = [];
110
- lines.forEach((line, index) => {
111
- if (line.includes(searchStr)) {
112
- matches.push(`Line ${index + 1}: ${this.truncate(line.trim(), 100)}`);
113
- }
114
- });
115
- const maxDisplay = 10;
116
- const result = matches.slice(0, maxDisplay).join('\n');
117
- if (matches.length > maxDisplay) {
118
- return result + `\n... and ${matches.length - maxDisplay} more`;
119
- }
120
- return result;
121
- }
122
- /**
123
- * Generate a unified diff showing changes
124
- * @param oldContent Original content
125
- * @param newContent Updated content
126
- * @param oldStr The string that was replaced
127
- * @param newStr The replacement string
128
- * @returns Formatted diff output
129
- */
130
- generateUnifiedDiff(oldContent, newContent, oldStr, newStr) {
131
- const oldLines = oldContent.split('\n');
132
- const newLines = newContent.split('\n');
133
- // Find changed lines
134
- const changedLineNumbers = [];
135
- oldLines.forEach((line, index) => {
136
- if (line.includes(oldStr)) {
137
- changedLineNumbers.push(index);
138
- }
139
- });
140
- // Build diff output
141
- const diffLines = [];
142
- changedLineNumbers.forEach(lineNum => {
143
- diffLines.push(`@@ Line ${lineNum + 1} @@`);
144
- diffLines.push(`- ${oldLines[lineNum]}`);
145
- diffLines.push(`+ ${newLines[lineNum]}`);
146
- diffLines.push('');
147
- });
148
- return diffLines.join('\n');
149
- }
150
- /**
151
- * Escape special regex characters
152
- * @param str String to escape
153
- * @returns Escaped string safe for use in regex
154
- */
155
- escapeRegExp(str) {
156
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
157
- }
158
- /**
159
- * Truncate a string for display
160
- * @param str String to truncate
161
- * @param maxLen Maximum length
162
- * @returns Truncated string with ellipsis if needed
163
- */
164
- truncate(str, maxLen) {
165
- return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
166
- }
167
- /**
168
- * Get all wikis in a project
169
- * @param project The project name
170
- * @returns List of wikis in the project
171
- */
172
- async getWikis(project) {
173
- this.validateProject(project);
174
- const response = await this.makeRequest(`${project}/_apis/wiki/wikis?api-version=${this.apiVersion}`);
175
- return {
176
- project,
177
- totalCount: response.value.length,
178
- wikis: response.value.map((wiki) => ({
179
- id: wiki.id,
180
- name: wiki.name,
181
- type: wiki.type,
182
- url: wiki.url,
183
- projectId: wiki.projectId,
184
- repositoryId: wiki.repositoryId,
185
- mappedPath: wiki.mappedPath
186
- }))
187
- };
188
- }
189
- /**
190
- * Search wiki pages across projects
191
- * @param searchText The text to search for
192
- * @param project Optional project filter
193
- * @param maxResults Maximum number of results (default: 25)
194
- * @returns Search results with highlighted content
195
- */
196
- async searchWikiPages(searchText, project, maxResults = 25) {
197
- if (project) {
198
- this.validateProject(project);
199
- }
200
- const searchBody = {
201
- searchText,
202
- $top: maxResults,
203
- $skip: 0
204
- };
205
- // Add project filter if specified
206
- if (project) {
207
- searchBody.filters = {
208
- Project: [project]
209
- };
210
- }
211
- const response = await this.makeRequest(`_apis/search/wikisearchresults?api-version=${this.apiVersion}`, 'POST', searchBody, true // Use search URL
212
- );
213
- return {
214
- searchText,
215
- project: project || 'all',
216
- totalCount: response.count || 0,
217
- results: (response.results || []).map((result) => {
218
- const gitPath = result.path;
219
- const wikiPath = this.convertGitPathToWikiPath(gitPath);
220
- return {
221
- fileName: result.fileName,
222
- gitPath: gitPath, // Original git path (for reference)
223
- path: wikiPath, // Wiki path (for get-page API) - kept as 'path' for backward compatibility
224
- wikiName: result.wiki?.name,
225
- wikiId: result.wiki?.id,
226
- project: result.project?.name,
227
- highlights: result.hits?.map((hit) => hit.highlights).flat() || []
228
- };
229
- })
230
- };
231
- }
232
- /**
233
- * Get a specific wiki page with content
234
- * @param project The project name
235
- * @param wikiId The wiki identifier (ID or name)
236
- * @param pagePath The path to the page (e.g., "/Setup/Authentication")
237
- * Accepts both wiki paths (with spaces) and git paths (with dashes and .md)
238
- * @param includeContent Include page content (default: true)
239
- * @returns Wiki page with content and metadata
240
- */
241
- async getWikiPage(project, wikiId, pagePath, includeContent = true) {
242
- this.validateProject(project);
243
- // Always normalize paths to wiki format (removes .md, converts dashes to spaces)
244
- // This ensures consistent behavior regardless of input format
245
- const wikiPath = this.convertGitPathToWikiPath(pagePath);
246
- // Log conversion if the path was changed (for debugging)
247
- if (wikiPath !== pagePath) {
248
- console.error(`Normalized wiki path: ${pagePath} -> ${wikiPath}`);
249
- }
250
- // Use axios directly to access response headers (for ETag)
251
- const url = `${this.baseUrl}/${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(wikiPath)}&includeContent=${includeContent}&api-version=${this.apiVersion}`;
252
- try {
253
- const axiosResponse = await axios({
254
- method: 'GET',
255
- url,
256
- headers: {
257
- 'Authorization': this.authHeader,
258
- 'Content-Type': 'application/json',
259
- 'Accept': 'application/json'
260
- }
261
- });
262
- const response = axiosResponse.data;
263
- // Extract ETag from response headers (needed for updates)
264
- const etag = axiosResponse.headers['etag'] || axiosResponse.headers['ETag'];
265
- // The API returns the page data directly (not wrapped in a 'page' property)
266
- return {
267
- id: response.id,
268
- path: response.path,
269
- content: response.content,
270
- gitItemPath: response.gitItemPath,
271
- subPages: response.subPages || [],
272
- url: response.url,
273
- remoteUrl: response.remoteUrl,
274
- version: etag, // Include ETag for use with updateWikiPage
275
- project,
276
- wikiId
277
- };
278
- }
279
- catch (error) {
280
- // Handle errors similar to makeRequest
281
- const errorDetails = error.response?.data?.message || error.response?.data || error.message;
282
- console.error('Azure DevOps API request failed:', {
283
- url,
284
- status: error.response?.status,
285
- error: errorDetails
286
- });
287
- if (error.response?.status === 401) {
288
- throw new Error('Azure DevOps authentication failed. Please check your PAT token and permissions.');
289
- }
290
- if (error.response?.status === 403) {
291
- throw new Error('Azure DevOps access denied. Please check your PAT scopes and project permissions.');
292
- }
293
- if (error.response?.status === 404) {
294
- throw new Error(`Wiki page not found: ${wikiPath} (original input: ${pagePath})`);
295
- }
296
- throw new Error(`Azure DevOps API request failed: ${error.message} - ${JSON.stringify(errorDetails)}`);
297
- }
298
- }
299
- /**
300
- * Create a new wiki page
301
- * @param project The project name
302
- * @param wikiId The wiki identifier
303
- * @param pagePath The path for the new page (will be normalized to wiki format)
304
- * @param content The markdown content
305
- * @returns Created page information
306
- */
307
- async createWikiPage(project, wikiId, pagePath, content) {
308
- this.validateProject(project);
309
- if (!this.config.enableWikiWrite) {
310
- throw new Error('Wiki write operations are disabled. Set AZUREDEVOPS_ENABLE_WIKI_WRITE=true to enable.');
311
- }
312
- // Always normalize paths to wiki format (removes .md, converts dashes to spaces)
313
- const wikiPath = this.convertGitPathToWikiPath(pagePath);
314
- // Log conversion if the path was changed (for debugging)
315
- if (wikiPath !== pagePath) {
316
- console.error(`Normalized wiki path for creation: ${pagePath} -> ${wikiPath}`);
317
- }
318
- const response = await this.makeRequest(`${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(wikiPath)}&api-version=${this.apiVersion}`, 'PUT', { content });
319
- return {
320
- id: response.page?.id,
321
- path: response.page?.path,
322
- gitItemPath: response.page?.gitItemPath,
323
- project,
324
- wikiId
325
- };
326
- }
327
- /**
328
- * Update an existing wiki page
329
- * @param project The project name
330
- * @param wikiId The wiki identifier
331
- * @param pagePath The path to the page (will be normalized to wiki format)
332
- * @param content The updated markdown content
333
- * @param version The ETag/version for optimistic concurrency. If not provided, will be auto-fetched from current page.
334
- * @returns Updated page information
335
- */
336
- async updateWikiPage(project, wikiId, pagePath, content, version) {
337
- this.validateProject(project);
338
- if (!this.config.enableWikiWrite) {
339
- throw new Error('Wiki write operations are disabled. Set AZUREDEVOPS_ENABLE_WIKI_WRITE=true to enable.');
340
- }
341
- // Always normalize paths to wiki format (removes .md, converts dashes to spaces)
342
- const wikiPath = this.convertGitPathToWikiPath(pagePath);
343
- // Log conversion if the path was changed (for debugging)
344
- if (wikiPath !== pagePath) {
345
- console.error(`Normalized wiki path for update: ${pagePath} -> ${wikiPath}`);
346
- }
347
- // Auto-fetch version if not provided (required for updating existing pages)
348
- if (!version) {
349
- try {
350
- const currentPage = await this.getWikiPage(project, wikiId, wikiPath, false);
351
- version = currentPage.version;
352
- if (version) {
353
- console.error(`Auto-fetched version for wiki update: ${version}`);
354
- }
355
- else {
356
- console.error(`Warning: getWikiPage returned no version/etag for existing page. Response: ${JSON.stringify(currentPage)}`);
357
- }
358
- }
359
- catch (error) {
360
- // If page doesn't exist, proceed without version (will create new page)
361
- const errorMsg = error.message?.toLowerCase() || '';
362
- if (!errorMsg.includes('not found') && !errorMsg.includes('404') && !errorMsg.includes('does not exist')) {
363
- throw error;
364
- }
365
- console.error(`Page not found, will create new page: ${wikiPath}`);
366
- }
367
- }
368
- // Add If-Match header if version is available (for optimistic concurrency control)
369
- const customHeaders = version ? { 'If-Match': version } : undefined;
370
- const response = await this.makeRequest(`${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(wikiPath)}&api-version=${this.apiVersion}`, 'PUT', { content }, false, // useSearchUrl
371
- customHeaders);
372
- return {
373
- id: response.page?.id,
374
- path: response.page?.path,
375
- gitItemPath: response.page?.gitItemPath,
376
- project,
377
- wikiId
378
- };
379
- }
380
- /**
381
- * Replace a specific string in a wiki page without rewriting entire content
382
- * @param project The project name
383
- * @param wikiId The wiki identifier
384
- * @param pagePath The path to the page (will be normalized to wiki format)
385
- * @param oldStr The exact string to replace
386
- * @param newStr The replacement string
387
- * @param replaceAll If true, replace all occurrences; if false, old_str must be unique
388
- * @param description Optional description of the change for audit logging
389
- * @returns Result with diff, occurrence count, version, and message
390
- */
391
- async strReplaceWikiPage(project, wikiId, pagePath, oldStr, newStr, replaceAll = false, description) {
392
- this.validateProject(project);
393
- // 1. Validate write permission
394
- if (!this.config.enableWikiWrite) {
395
- throw new Error('Wiki write operations are disabled. Set AZUREDEVOPS_ENABLE_WIKI_WRITE=true to enable.');
396
- }
397
- // 2. Fetch current page content and version (auto-fetch latest)
398
- const currentPage = await this.getWikiPage(project, wikiId, pagePath, true);
399
- const currentContent = currentPage.content;
400
- const currentVersion = currentPage.version;
401
- // 3. Count occurrences of old_str
402
- const occurrences = this.countOccurrences(currentContent, oldStr);
403
- if (occurrences === 0) {
404
- throw new Error(`String not found in page.\n\n` +
405
- `Looking for: "${this.truncate(oldStr, 200)}"\n\n` +
406
- `Page excerpt:\n${this.truncate(currentContent, 500)}`);
407
- }
408
- if (occurrences > 1 && !replaceAll) {
409
- throw new Error(`String appears ${occurrences} times in the page. ` +
410
- `Either provide more context to make old_str unique, or set replace_all=true.\n\n` +
411
- `Matching locations:\n${this.getMatchLocations(currentContent, oldStr)}`);
412
- }
413
- // 4. Perform replacement
414
- const regex = new RegExp(this.escapeRegExp(oldStr), replaceAll ? 'g' : '');
415
- const newContent = currentContent.replace(regex, newStr);
416
- // 5. Validate replacement succeeded
417
- if (newContent === currentContent) {
418
- throw new Error('Replacement failed - content unchanged');
419
- }
420
- // 6. Update wiki page with version conflict retry
421
- let updateResult;
422
- try {
423
- updateResult = await this.updateWikiPage(project, wikiId, pagePath, newContent, currentVersion);
424
- }
425
- catch (error) {
426
- // Version conflict - retry once with fresh version
427
- if (error.message.includes('412') || error.message.includes('version') || error.message.includes('conflict')) {
428
- console.error('Version conflict detected, retrying with fresh version...');
429
- const freshPage = await this.getWikiPage(project, wikiId, pagePath, true);
430
- const freshContent = freshPage.content;
431
- const freshVersion = freshPage.version;
432
- // Re-apply replacement to fresh content
433
- const freshRegex = new RegExp(this.escapeRegExp(oldStr), replaceAll ? 'g' : '');
434
- const freshNewContent = freshContent.replace(freshRegex, newStr);
435
- updateResult = await this.updateWikiPage(project, wikiId, pagePath, freshNewContent, freshVersion);
436
- }
437
- else {
438
- throw error;
439
- }
440
- }
441
- // 7. Generate diff output
442
- const diff = this.generateUnifiedDiff(currentContent, newContent, oldStr, newStr);
443
- // 8. Return result with diff
444
- return {
445
- success: true,
446
- diff,
447
- occurrences: replaceAll ? occurrences : 1,
448
- version: currentVersion,
449
- message: `Successfully replaced ${replaceAll ? occurrences : 1} occurrence(s)`,
450
- ...updateResult
451
- };
452
- }
453
- /**
454
- * Delete a wiki page permanently
455
- * @param project The project name
456
- * @param wikiId The wiki identifier
457
- * @param pagePath The path to the page (used as-is, no conversion applied)
458
- * @returns Deletion confirmation
459
- */
460
- async deleteWikiPage(project, wikiId, pagePath) {
461
- this.validateProject(project);
462
- if (!this.config.enableWikiDelete) {
463
- throw new Error('Wiki delete operations are disabled. Set AZUREDEVOPS_ENABLE_WIKI_DELETE=true to enable.');
464
- }
465
- // NOTE: Unlike other wiki methods, we do NOT convert the path here.
466
- // The user provides the actual wiki path (with hyphens, spaces, etc.) directly.
467
- // Converting hyphens to spaces would break paths with dates (e.g., "2026-02-03")
468
- // or other legitimate hyphens in page names.
469
- await this.makeRequest(`${project}/_apis/wiki/wikis/${wikiId}/pages?path=${encodeURIComponent(pagePath)}&api-version=${this.apiVersion}`, 'DELETE');
470
- return {
471
- project,
472
- wikiId,
473
- pagePath: pagePath,
474
- deleted: true
475
- };
476
- }
477
- // ==================== WORK ITEM OPERATIONS ====================
478
- /**
479
- * Get a work item by ID with full details
480
- * @param project The project name
481
- * @param workItemId The work item ID
482
- * @returns Complete work item details
483
- */
484
- async getWorkItem(project, workItemId) {
485
- this.validateProject(project);
486
- const response = await this.makeRequest(`${project}/_apis/wit/workitems/${workItemId}?$expand=all&api-version=${this.apiVersion}`);
487
- return {
488
- id: response.id,
489
- rev: response.rev,
490
- url: response.url,
491
- fields: response.fields,
492
- relations: response.relations || [],
493
- _links: response._links,
494
- commentVersionRef: response.commentVersionRef,
495
- project
496
- };
497
- }
498
- /**
499
- * Query work items using WIQL (Work Item Query Language)
500
- * @param project The project name
501
- * @param wiql The WIQL query string
502
- * @param maxResults Maximum number of results (default: 200)
503
- * @returns Work items matching the query
504
- */
505
- async queryWorkItems(project, wiql, maxResults = 200) {
506
- this.validateProject(project);
507
- // Execute WIQL query
508
- const queryResult = await this.makeRequest(`${project}/_apis/wit/wiql?api-version=${this.apiVersion}`, 'POST', { query: wiql });
509
- if (!queryResult.workItems || queryResult.workItems.length === 0) {
510
- return {
511
- query: wiql,
512
- project,
513
- totalCount: 0,
514
- workItems: []
515
- };
516
- }
517
- // Get work item IDs (limit to maxResults)
518
- const workItemIds = queryResult.workItems
519
- .slice(0, maxResults)
520
- .map((wi) => wi.id);
521
- // Batch get full work item details
522
- const workItems = await this.makeRequest(`${project}/_apis/wit/workitemsbatch?api-version=${this.apiVersion}`, 'POST', {
523
- ids: workItemIds,
524
- $expand: 'all'
525
- });
526
- return {
527
- query: wiql,
528
- project,
529
- totalCount: workItems.value.length,
530
- workItems: workItems.value
531
- };
532
- }
533
- static SUMMARY_FIELDS = [
534
- 'System.Id',
535
- 'System.Title',
536
- 'System.AssignedTo',
537
- 'System.State',
538
- 'Microsoft.VSTS.Common.Severity',
539
- 'Microsoft.VSTS.Common.Priority',
540
- 'System.Tags',
541
- 'Microsoft.VSTS.Scheduling.StoryPoints',
542
- 'Microsoft.VSTS.Common.ResolvedReason',
543
- 'System.WorkItemType',
544
- ];
545
- async runSavedQuery(project, queryId, maxResults = 50, detail = 'summary', fields, groupBy) {
546
- this.validateProject(project);
547
- const queryResult = await this.makeRequest(`${project}/_apis/wit/wiql/${queryId}?api-version=${this.apiVersion}`, 'GET');
548
- if (!queryResult.workItems || queryResult.workItems.length === 0) {
549
- return {
550
- queryId,
551
- project,
552
- totalCount: 0,
553
- queriedCount: queryResult.workItems?.length ?? 0,
554
- workItems: [],
555
- };
556
- }
557
- const totalAvailable = queryResult.workItems.length;
558
- const workItemIds = queryResult.workItems
559
- .slice(0, maxResults)
560
- .map((wi) => wi.id);
561
- const isFull = detail === 'full';
562
- const requestedFields = fields ?? (isFull ? undefined : AzureDevOpsService.SUMMARY_FIELDS);
563
- const body = { ids: workItemIds };
564
- if (requestedFields) {
565
- body.fields = requestedFields;
566
- }
567
- else {
568
- body.$expand = 'all';
569
- }
570
- const workItems = await this.makeRequest(`${project}/_apis/wit/workitemsbatch?api-version=${this.apiVersion}`, 'POST', body);
571
- const items = workItems.value.map((wi) => {
572
- if (isFull && !fields)
573
- return wi;
574
- const f = wi.fields || {};
575
- return {
576
- id: wi.id,
577
- type: f['System.WorkItemType'],
578
- title: f['System.Title'],
579
- assignedTo: f['System.AssignedTo']?.displayName ?? f['System.AssignedTo'] ?? null,
580
- state: f['System.State'],
581
- severity: f['Microsoft.VSTS.Common.Severity'] ?? null,
582
- priority: f['Microsoft.VSTS.Common.Priority'] ?? null,
583
- tags: f['System.Tags'] ?? null,
584
- storyPoints: f['Microsoft.VSTS.Scheduling.StoryPoints'] ?? null,
585
- resolvedReason: f['Microsoft.VSTS.Common.ResolvedReason'] ?? null,
586
- ...(fields ? Object.fromEntries(Object.entries(f).filter(([k]) => !AzureDevOpsService.SUMMARY_FIELDS.includes(k))) : {}),
587
- };
588
- });
589
- const result = {
590
- queryId,
591
- project,
592
- totalAvailable,
593
- returnedCount: items.length,
594
- detail,
595
- };
596
- if (groupBy) {
597
- const groups = {};
598
- for (const item of items) {
599
- const key = item[groupBy] ?? item.state ?? 'Unknown';
600
- if (!groups[key])
601
- groups[key] = [];
602
- groups[key].push(item);
603
- }
604
- result.groupedBy = groupBy;
605
- result.groups = Object.fromEntries(Object.entries(groups).map(([k, v]) => [k, { count: v.length, items: v }]));
606
- }
607
- else {
608
- result.workItems = items;
609
- }
610
- return result;
611
- }
612
- async getSavedQuery(project, queryId) {
613
- this.validateProject(project);
614
- return this.makeRequest(`${project}/_apis/wit/queries/${queryId}?api-version=${this.apiVersion}`, 'GET');
615
- }
616
- /**
617
- * Get comments/discussion for a work item
618
- * @param project The project name
619
- * @param workItemId The work item ID
620
- * @returns List of comments
621
- */
622
- async getWorkItemComments(project, workItemId) {
623
- this.validateProject(project);
624
- // Comments API requires -preview suffix (not GA in 7.1)
625
- const response = await this.makeRequest(`${project}/_apis/wit/workItems/${workItemId}/comments?api-version=7.1-preview`);
626
- // v7.1 Comments API returns array in response.comments, not response.value
627
- const comments = response.comments || response.value || [];
628
- return {
629
- workItemId,
630
- project,
631
- totalCount: response.totalCount ?? comments.length,
632
- comments: comments.map((comment) => ({
633
- id: comment.id,
634
- text: comment.text,
635
- createdBy: comment.createdBy?.displayName,
636
- createdDate: comment.createdDate,
637
- modifiedBy: comment.modifiedBy?.displayName,
638
- modifiedDate: comment.modifiedDate,
639
- url: comment.url
640
- }))
641
- };
642
- }
643
- /**
644
- * Add a comment to a work item
645
- * @param project The project name
646
- * @param workItemId The work item ID
647
- * @param commentText The comment text (supports markdown)
648
- * @returns Created comment information
649
- */
650
- async addWorkItemComment(project, workItemId, commentText) {
651
- this.validateProject(project);
652
- if (!this.config.enableWorkItemWrite) {
653
- throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
654
- }
655
- // Convert Markdown to HTML if configured for legacy orgs
656
- let finalText = commentText;
657
- const format = this.config.commentFormat || 'markdown';
658
- if (format === 'html') {
659
- // Convert Markdown to HTML for organizations without Markdown preview
660
- finalText = await marked.parse(commentText);
661
- }
662
- // Comments API requires -preview suffix (not GA in 7.1)
663
- const response = await this.makeRequest(`${project}/_apis/wit/workItems/${workItemId}/comments?api-version=7.1-preview`, 'POST', { text: finalText });
664
- return {
665
- id: response.id,
666
- workItemId,
667
- project,
668
- text: response.text,
669
- format: format,
670
- createdBy: response.createdBy?.displayName,
671
- createdDate: response.createdDate
672
- };
673
- }
674
- /**
675
- * Update a work item using JSON Patch operations
676
- * @param project The project name
677
- * @param workItemId The work item ID
678
- * @param patchOperations Array of JSON Patch operations
679
- * @returns Updated work item
680
- */
681
- async updateWorkItem(project, workItemId, patchOperations) {
682
- this.validateProject(project);
683
- if (!this.config.enableWorkItemWrite) {
684
- throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
685
- }
686
- const response = await this.makeRequest(`${project}/_apis/wit/workitems/${workItemId}?api-version=${this.apiVersion}`, 'PATCH', patchOperations);
687
- return {
688
- id: response.id,
689
- rev: response.rev,
690
- fields: response.fields,
691
- project
692
- };
693
- }
694
- /**
695
- * Set work item field(s) to markdown format.
696
- * This is IRREVERSIBLE - once set to markdown, cannot revert to HTML.
697
- *
698
- * @param project - Project name
699
- * @param workItemId - Work item ID
700
- * @param fields - Array of field names to set to markdown format (e.g., ['System.Description'])
701
- */
702
- async setFieldsToMarkdownFormat(project, workItemId, fields) {
703
- if (!this.config.enableWorkItemWrite) {
704
- throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
705
- }
706
- const patchOperations = fields.map(field => ({
707
- op: 'add',
708
- path: `/multilineFieldsFormat/${field}`,
709
- value: 'Markdown'
710
- }));
711
- await this.updateWorkItem(project, workItemId, patchOperations);
712
- }
713
- /**
714
- * Create a new work item
715
- * @param project The project name
716
- * @param workItemType The work item type (e.g., "Bug", "Task", "User Story")
717
- * @param fields Object with field values (e.g., { "System.Title": "Bug title" })
718
- * @param parentId Optional parent work item ID (for creating child items)
719
- * @param relations Optional array of work item relationships
720
- * @returns Created work item
721
- */
722
- async createWorkItem(project, workItemType, fields, parentId, relations) {
723
- this.validateProject(project);
724
- if (!this.config.enableWorkItemWrite) {
725
- throw new Error('Work item write operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_WRITE=true to enable.');
726
- }
727
- // Build patch operations array
728
- const patchOperations = [];
729
- // Add field operations
730
- Object.keys(fields).forEach(field => {
731
- patchOperations.push({
732
- op: 'add',
733
- path: `/fields/${field}`,
734
- value: fields[field]
735
- });
736
- });
737
- // Auto-set markdown format for large text fields that are being set
738
- // This ensures all new work items use markdown format (irreversible)
739
- // Uses getAllLargeTextFields() to include custom fields like Custom.Howtotest
740
- const allLargeTextFields = getAllLargeTextFields();
741
- for (const field of allLargeTextFields) {
742
- if (fields[field] !== undefined) {
743
- patchOperations.push({
744
- op: 'add',
745
- path: `/multilineFieldsFormat/${field}`,
746
- value: 'Markdown'
747
- });
748
- }
749
- }
750
- // Handle parentId parameter (simplified parent relationship)
751
- if (parentId !== undefined) {
752
- const parentUrl = `${this.baseUrl}/${encodeURIComponent(project)}/_apis/wit/workItems/${parentId}`;
753
- patchOperations.push({
754
- op: 'add',
755
- path: '/relations/-',
756
- value: {
757
- rel: 'System.LinkTypes.Hierarchy-Reverse',
758
- url: parentUrl
759
- }
760
- });
761
- }
762
- // Handle relations array (advanced relationships)
763
- if (relations && relations.length > 0) {
764
- relations.forEach(relation => {
765
- patchOperations.push({
766
- op: 'add',
767
- path: '/relations/-',
768
- value: relation
769
- });
770
- });
771
- }
772
- const response = await this.makeRequest(`${project}/_apis/wit/workitems/$${workItemType}?api-version=${this.apiVersion}`, 'PATCH', patchOperations);
773
- return {
774
- id: response.id,
775
- rev: response.rev,
776
- fields: response.fields,
777
- relations: response.relations || [], // Include relations in response
778
- url: response._links?.html?.href,
779
- project
780
- };
781
- }
782
- /**
783
- * Delete a work item
784
- * @param project The project name
785
- * @param workItemId The work item ID
786
- * @returns Deletion confirmation
787
- */
788
- async deleteWorkItem(project, workItemId) {
789
- this.validateProject(project);
790
- if (!this.config.enableWorkItemDelete) {
791
- throw new Error('Work item delete operations are disabled. Set AZUREDEVOPS_ENABLE_WORK_ITEM_DELETE=true to enable.');
792
- }
793
- await this.makeRequest(`${project}/_apis/wit/workitems/${workItemId}?api-version=${this.apiVersion}`, 'DELETE');
794
- return {
795
- workItemId,
796
- project,
797
- deleted: true
798
- };
799
- }
800
- // ==================== VARIABLE GROUP OPERATIONS ====================
801
- /**
802
- * Get all variable groups in a project
803
- * @param project The project name
804
- * @returns List of variable groups with their variables
805
- */
806
- async getVariableGroups(project) {
807
- this.validateProject(project);
808
- const response = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups?api-version=${this.apiVersion}`);
809
- return {
810
- project,
811
- totalCount: response.value.length,
812
- variableGroups: response.value.map((group) => ({
813
- id: group.id,
814
- name: group.name,
815
- description: group.description,
816
- type: group.type,
817
- createdBy: group.createdBy?.displayName,
818
- createdOn: group.createdOn,
819
- modifiedBy: group.modifiedBy?.displayName,
820
- modifiedOn: group.modifiedOn,
821
- isShared: group.isShared,
822
- variableGroupProjectReferences: group.variableGroupProjectReferences,
823
- // Include variable names but mask secret values
824
- variables: Object.keys(group.variables || {}).reduce((acc, key) => {
825
- const variable = group.variables[key];
826
- acc[key] = {
827
- value: variable.isSecret ? '***SECRET***' : variable.value,
828
- isSecret: variable.isSecret || false,
829
- isReadOnly: variable.isReadOnly || false
830
- };
831
- return acc;
832
- }, {})
833
- }))
834
- };
835
- }
836
- /**
837
- * Get a specific variable group by ID
838
- * @param project The project name
839
- * @param groupId The variable group ID
840
- * @returns Variable group details with variables
841
- */
842
- async getVariableGroup(project, groupId) {
843
- this.validateProject(project);
844
- const response = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`);
845
- return {
846
- id: response.id,
847
- name: response.name,
848
- description: response.description,
849
- type: response.type,
850
- createdBy: response.createdBy?.displayName,
851
- createdOn: response.createdOn,
852
- modifiedBy: response.modifiedBy?.displayName,
853
- modifiedOn: response.modifiedOn,
854
- isShared: response.isShared,
855
- variableGroupProjectReferences: response.variableGroupProjectReferences,
856
- project,
857
- // Include variable names but mask secret values
858
- variables: Object.keys(response.variables || {}).reduce((acc, key) => {
859
- const variable = response.variables[key];
860
- acc[key] = {
861
- value: variable.isSecret ? '***SECRET***' : variable.value,
862
- isSecret: variable.isSecret || false,
863
- isReadOnly: variable.isReadOnly || false
864
- };
865
- return acc;
866
- }, {})
867
- };
868
- }
869
- // ═══════════════════════════════════════════════════════════════════════════════
870
- // BUILD TROUBLESHOOTING OPERATIONS (Read-only)
871
- // NOTE: These methods are duplicated in azure-devops-admin package.
872
- // If you update these, also update packages/azure-devops-admin/src/AzureDevOpsAdminService.ts
873
- // ═══════════════════════════════════════════════════════════════════════════════
874
- /**
875
- * Get build status with optional timeline and logs
876
- * @param project The project name
877
- * @param buildId The build ID
878
- * @param detail Level of detail: 'summary', 'timeline', or 'full'
879
- * @param timelineScope Scope for timeline: 'stages', 'jobs', 'all', 'problems'
880
- * @param maxIssues Maximum issues per record
881
- */
882
- async getBuildStatus(project, buildId, detail = 'summary', timelineScope = 'problems', maxIssues = 5) {
883
- this.validateProject(project);
884
- const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}?api-version=${this.apiVersion}`);
885
- const result = {
886
- id: response.id,
887
- buildNumber: response.buildNumber,
888
- status: response.status,
889
- result: response.result,
890
- queueTime: response.queueTime,
891
- startTime: response.startTime,
892
- finishTime: response.finishTime,
893
- sourceBranch: response.sourceBranch,
894
- sourceVersion: response.sourceVersion,
895
- definition: response.definition ? {
896
- id: response.definition.id,
897
- name: response.definition.name
898
- } : null,
899
- requestedBy: response.requestedBy?.displayName,
900
- requestedFor: response.requestedFor?.displayName,
901
- reason: response.reason,
902
- priority: response.priority,
903
- project: response.project?.name,
904
- url: response._links?.web?.href
905
- };
906
- if (detail === 'timeline' || detail === 'full') {
907
- const timeline = await this.getBuildTimeline(project, buildId, timelineScope, maxIssues);
908
- result.timeline = timeline;
909
- }
910
- if (detail === 'full') {
911
- const logs = await this.getBuildLogs(project, buildId);
912
- result.logs = logs;
913
- }
914
- return result;
915
- }
916
- /**
917
- * Get build timeline with step-by-step breakdown
918
- * @param project The project name
919
- * @param buildId The build ID
920
- * @param scope Filter scope: 'stages', 'jobs', 'all', 'problems'
921
- * @param maxIssues Maximum issues per record
922
- */
923
- async getBuildTimeline(project, buildId, scope = 'problems', maxIssues = 5) {
924
- this.validateProject(project);
925
- const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/timeline?api-version=${this.apiVersion}`);
926
- const allRecords = response.records || [];
927
- const summary = {
928
- total: allRecords.length,
929
- byType: {},
930
- byResult: {},
931
- totalErrors: 0,
932
- totalWarnings: 0,
933
- failed: [],
934
- };
935
- for (const record of allRecords) {
936
- summary.byType[record.type] = (summary.byType[record.type] || 0) + 1;
937
- if (record.result) {
938
- summary.byResult[record.result] = (summary.byResult[record.result] || 0) + 1;
939
- }
940
- summary.totalErrors += record.errorCount || 0;
941
- summary.totalWarnings += record.warningCount || 0;
942
- if (record.result === 'failed' || record.result === 'canceled') {
943
- summary.failed.push(`${record.type}: ${record.name}`);
944
- }
945
- }
946
- let filteredRecords = allRecords;
947
- switch (scope) {
948
- case 'stages':
949
- filteredRecords = allRecords.filter((r) => r.type === 'Stage');
950
- break;
951
- case 'jobs':
952
- filteredRecords = allRecords.filter((r) => r.type === 'Stage' || r.type === 'Job');
953
- break;
954
- case 'problems':
955
- filteredRecords = allRecords.filter((r) => (r.errorCount && r.errorCount > 0) ||
956
- (r.warningCount && r.warningCount > 0) ||
957
- r.result === 'failed' ||
958
- r.result === 'canceled');
959
- break;
960
- case 'all':
961
- default:
962
- break;
963
- }
964
- const truncateIssues = (issues, max) => {
965
- if (!issues || issues.length === 0)
966
- return { items: [], totalCount: 0, truncated: false };
967
- const sorted = [...issues].sort((a, b) => {
968
- const priority = (issue) => {
969
- if (issue.type === 'error')
970
- return 0;
971
- if (issue.type === 'warning')
972
- return 1;
973
- return 2;
974
- };
975
- return priority(a) - priority(b);
976
- });
977
- return {
978
- items: sorted.slice(0, max),
979
- totalCount: issues.length,
980
- truncated: issues.length > max
981
- };
982
- };
983
- const mappedRecords = filteredRecords.map((record) => {
984
- const truncatedIssues = truncateIssues(record.issues, maxIssues);
985
- return {
986
- id: record.id,
987
- parentId: record.parentId,
988
- type: record.type,
989
- name: record.name,
990
- state: record.state,
991
- result: record.result,
992
- startTime: record.startTime,
993
- finishTime: record.finishTime,
994
- order: record.order,
995
- errorCount: record.errorCount,
996
- warningCount: record.warningCount,
997
- log: record.log ? { id: record.log.id } : null,
998
- issues: truncatedIssues.items,
999
- issuesTruncated: truncatedIssues.truncated,
1000
- totalIssueCount: truncatedIssues.totalCount
1001
- };
1002
- });
1003
- return {
1004
- buildId,
1005
- project,
1006
- scope,
1007
- summary,
1008
- recordCount: mappedRecords.length,
1009
- records: mappedRecords
1010
- };
1011
- }
1012
- /**
1013
- * Filter log content to reduce noise from progress indicators
1014
- * @param content Raw log content string
1015
- * @param mode Filter mode: 'summary' removes progress, 'errors' shows only errors, 'full' returns everything
1016
- */
1017
- filterLogContent(content, mode) {
1018
- const lines = content.split('\n');
1019
- const originalLineCount = lines.length;
1020
- if (mode === 'full') {
1021
- return { filtered: content, originalLineCount, filteredLineCount: originalLineCount };
1022
- }
1023
- // Progress indicator patterns to remove in summary mode
1024
- const PROGRESS_PATTERNS = [
1025
- /remote: Counting objects:\s+\d+%/,
1026
- /remote: Compressing objects:\s+\d+%/,
1027
- /Receiving objects:\s+\d+%/,
1028
- /Resolving deltas:\s+\d+%/,
1029
- /Unpacking objects:\s+\d+%/,
1030
- /Updating files:\s+\d+%/,
1031
- ];
1032
- // Error/warning patterns for errors mode
1033
- const ERROR_PATTERNS = [
1034
- /##\[error\]/i,
1035
- /##\[warning\]/i,
1036
- /\berror\b.*:/i,
1037
- /\bfailed\b/i,
1038
- /\bexception\b/i,
1039
- /\bfatal\b/i,
1040
- ];
1041
- const filteredLines = lines.filter(line => {
1042
- // Remove timestamp prefix for pattern matching
1043
- const trimmedLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, '');
1044
- if (mode === 'errors') {
1045
- return ERROR_PATTERNS.some(p => p.test(trimmedLine));
1046
- }
1047
- // summary mode: exclude progress indicators, keep everything else
1048
- return !PROGRESS_PATTERNS.some(p => p.test(trimmedLine));
1049
- });
1050
- return {
1051
- filtered: filteredLines.join('\n'),
1052
- originalLineCount,
1053
- filteredLineCount: filteredLines.length
1054
- };
1055
- }
1056
- /**
1057
- * Get build logs (list or specific log content)
1058
- * @param project The project name
1059
- * @param buildId The build ID
1060
- * @param logId Optional specific log ID to retrieve content
1061
- * @param mode Filter mode: 'summary' (default) removes progress indicators, 'full' returns everything, 'errors' shows only errors/warnings
1062
- */
1063
- async getBuildLogs(project, buildId, logId, mode = 'summary') {
1064
- this.validateProject(project);
1065
- if (logId !== undefined) {
1066
- const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/logs/${logId}?api-version=${this.apiVersion}`);
1067
- const { filtered, originalLineCount, filteredLineCount } = this.filterLogContent(response, mode);
1068
- return {
1069
- buildId,
1070
- logId,
1071
- project,
1072
- mode,
1073
- originalLineCount,
1074
- filteredLineCount,
1075
- content: filtered
1076
- };
1077
- }
1078
- const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/logs?api-version=${this.apiVersion}`);
1079
- return {
1080
- buildId,
1081
- project,
1082
- totalCount: response.value.length,
1083
- logs: response.value.map((log) => ({
1084
- id: log.id,
1085
- type: log.type,
1086
- lineCount: log.lineCount,
1087
- createdOn: log.createdOn,
1088
- lastChangedOn: log.lastChangedOn,
1089
- url: log.url
1090
- }))
1091
- };
1092
- }
1093
- // ═══════════════════════════════════════════════════════════════════════════════
1094
- // PULL REQUEST OPERATIONS
1095
- // ═══════════════════════════════════════════════════════════════════════════════
1096
- /**
1097
- * List all Git repositories in a project
1098
- * @param project The project name
1099
- * @returns List of repositories with their IDs
1100
- */
1101
- async listRepositories(project) {
1102
- this.validateProject(project);
1103
- const response = await this.makeRequest(`${project}/_apis/git/repositories?api-version=${this.apiVersion}`);
1104
- return {
1105
- project,
1106
- totalCount: response.value.length,
1107
- repositories: response.value.map((repo) => ({
1108
- id: repo.id,
1109
- name: repo.name,
1110
- url: repo.url,
1111
- defaultBranch: repo.defaultBranch,
1112
- size: repo.size,
1113
- remoteUrl: repo.remoteUrl,
1114
- webUrl: repo.webUrl
1115
- }))
1116
- };
1117
- }
1118
- /**
1119
- * List pull requests in a repository
1120
- * @param project The project name
1121
- * @param repositoryId Repository ID (GUID) or name
1122
- * @param status Filter by status: active, completed, abandoned, all (default: active)
1123
- * @param top Maximum results (default: 25)
1124
- * @param creatorId Filter by creator ID
1125
- * @param reviewerId Filter by reviewer ID
1126
- * @returns List of pull requests
1127
- */
1128
- async listPullRequests(project, repositoryId, status = 'active', top = 25, creatorId, reviewerId) {
1129
- this.validateProject(project);
1130
- let url = `${project}/_apis/git/repositories/${repositoryId}/pullrequests?searchCriteria.status=${status}&$top=${top}&api-version=${this.apiVersion}`;
1131
- if (creatorId)
1132
- url += `&searchCriteria.creatorId=${creatorId}`;
1133
- if (reviewerId)
1134
- url += `&searchCriteria.reviewerId=${reviewerId}`;
1135
- const response = await this.makeRequest(url);
1136
- return {
1137
- project,
1138
- repositoryId,
1139
- status,
1140
- totalCount: response.value.length,
1141
- pullRequests: response.value.map((pr) => ({
1142
- pullRequestId: pr.pullRequestId,
1143
- title: pr.title,
1144
- description: pr.description ? this.truncate(pr.description, 200) : null,
1145
- status: pr.status,
1146
- createdBy: pr.createdBy?.displayName,
1147
- creationDate: pr.creationDate,
1148
- closedDate: pr.closedDate,
1149
- sourceBranch: pr.sourceRefName?.replace('refs/heads/', ''),
1150
- targetBranch: pr.targetRefName?.replace('refs/heads/', ''),
1151
- mergeStatus: pr.mergeStatus,
1152
- isDraft: pr.isDraft,
1153
- reviewerCount: pr.reviewers?.length || 0,
1154
- url: pr._links?.web?.href
1155
- }))
1156
- };
1157
- }
1158
- /**
1159
- * Get details of a specific pull request
1160
- * @param project The project name
1161
- * @param repositoryId Repository ID (GUID) or name
1162
- * @param pullRequestId The PR ID
1163
- * @returns Pull request details with reviewers
1164
- */
1165
- async getPullRequest(project, repositoryId, pullRequestId) {
1166
- this.validateProject(project);
1167
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}?api-version=${this.apiVersion}`);
1168
- return {
1169
- pullRequestId: response.pullRequestId,
1170
- title: response.title,
1171
- description: response.description,
1172
- status: response.status,
1173
- createdBy: {
1174
- displayName: response.createdBy?.displayName,
1175
- id: response.createdBy?.id,
1176
- uniqueName: response.createdBy?.uniqueName
1177
- },
1178
- creationDate: response.creationDate,
1179
- closedDate: response.closedDate,
1180
- sourceBranch: response.sourceRefName?.replace('refs/heads/', ''),
1181
- targetBranch: response.targetRefName?.replace('refs/heads/', ''),
1182
- mergeStatus: response.mergeStatus,
1183
- isDraft: response.isDraft,
1184
- mergeId: response.lastMergeCommit?.commitId,
1185
- sourceCommitId: response.lastMergeSourceCommit?.commitId,
1186
- targetCommitId: response.lastMergeTargetCommit?.commitId,
1187
- supportsIterations: response.supportsIterations,
1188
- reviewers: (response.reviewers || []).map((r) => ({
1189
- displayName: r.displayName,
1190
- id: r.id,
1191
- vote: r.vote,
1192
- voteLabel: this.getVoteLabel(r.vote),
1193
- isRequired: r.isRequired,
1194
- hasDeclined: r.hasDeclined
1195
- })),
1196
- labels: response.labels?.map((l) => l.name) || [],
1197
- autoComplete: response.autoCompleteSetBy ? {
1198
- setBy: response.autoCompleteSetBy.displayName,
1199
- mergeStrategy: response.completionOptions?.mergeStrategy
1200
- } : null,
1201
- url: response._links?.web?.href,
1202
- project
1203
- };
1204
- }
1205
- /**
1206
- * Convert numeric vote to label
1207
- */
1208
- getVoteLabel(vote) {
1209
- switch (vote) {
1210
- case -10: return 'Rejected';
1211
- case -5: return 'Waiting for author';
1212
- case 0: return 'No response';
1213
- case 5: return 'Approved with suggestions';
1214
- case 10: return 'Approved';
1215
- default: return `Unknown (${vote})`;
1216
- }
1217
- }
1218
- /**
1219
- * Get commits in a pull request
1220
- * @param project The project name
1221
- * @param repositoryId Repository ID (GUID) or name
1222
- * @param pullRequestId The PR ID
1223
- * @returns List of commits
1224
- */
1225
- async getPullRequestCommits(project, repositoryId, pullRequestId) {
1226
- this.validateProject(project);
1227
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/commits?api-version=${this.apiVersion}`);
1228
- return {
1229
- pullRequestId,
1230
- project,
1231
- totalCount: response.value.length,
1232
- commits: response.value.map((c) => ({
1233
- commitId: c.commitId,
1234
- comment: c.comment,
1235
- author: {
1236
- name: c.author?.name,
1237
- email: c.author?.email,
1238
- date: c.author?.date
1239
- },
1240
- committer: {
1241
- name: c.committer?.name,
1242
- date: c.committer?.date
1243
- },
1244
- url: c.url
1245
- }))
1246
- };
1247
- }
1248
- /**
1249
- * Get threads/comments on a pull request
1250
- * @param project The project name
1251
- * @param repositoryId Repository ID (GUID) or name
1252
- * @param pullRequestId The PR ID
1253
- * @returns List of discussion threads
1254
- */
1255
- async getPullRequestThreads(project, repositoryId, pullRequestId) {
1256
- this.validateProject(project);
1257
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/threads?api-version=${this.apiVersion}`);
1258
- return {
1259
- pullRequestId,
1260
- project,
1261
- totalCount: response.value.length,
1262
- threads: response.value.map((t) => ({
1263
- id: t.id,
1264
- status: t.status,
1265
- publishedDate: t.publishedDate,
1266
- lastUpdatedDate: t.lastUpdatedDate,
1267
- isDeleted: t.isDeleted,
1268
- threadContext: t.threadContext ? {
1269
- filePath: t.threadContext.filePath,
1270
- rightFileStart: t.threadContext.rightFileStart,
1271
- rightFileEnd: t.threadContext.rightFileEnd
1272
- } : null,
1273
- comments: (t.comments || []).filter((c) => !c.isDeleted).map((c) => ({
1274
- id: c.id,
1275
- author: c.author?.displayName,
1276
- content: c.content,
1277
- publishedDate: c.publishedDate,
1278
- commentType: c.commentType,
1279
- parentCommentId: c.parentCommentId
1280
- }))
1281
- }))
1282
- };
1283
- }
1284
- /**
1285
- * Get file changes in a pull request
1286
- * @param project The project name
1287
- * @param repositoryId Repository ID (GUID) or name
1288
- * @param pullRequestId The PR ID
1289
- * @param iterationId Iteration ID (default: latest)
1290
- * @returns List of changed files
1291
- */
1292
- async getPullRequestChanges(project, repositoryId, pullRequestId, iterationId) {
1293
- this.validateProject(project);
1294
- // If no iteration specified, get the latest
1295
- let targetIteration = iterationId;
1296
- if (!targetIteration) {
1297
- const iterations = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/iterations?api-version=${this.apiVersion}`);
1298
- if (iterations.value.length > 0) {
1299
- targetIteration = iterations.value[iterations.value.length - 1].id;
1300
- }
1301
- else {
1302
- throw new Error('No iterations found for this pull request');
1303
- }
1304
- }
1305
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/iterations/${targetIteration}/changes?api-version=${this.apiVersion}`);
1306
- return {
1307
- pullRequestId,
1308
- iterationId: targetIteration,
1309
- project,
1310
- totalCount: response.changeEntries?.length || 0,
1311
- changes: (response.changeEntries || []).map((c) => ({
1312
- changeType: c.changeType,
1313
- path: c.item?.path,
1314
- originalPath: c.originalPath,
1315
- objectId: c.item?.objectId,
1316
- originalObjectId: c.item?.originalObjectId
1317
- }))
1318
- };
1319
- }
1320
- /**
1321
- * Add a comment thread to a pull request
1322
- * @param project The project name
1323
- * @param repositoryId Repository ID (GUID) or name
1324
- * @param pullRequestId The PR ID
1325
- * @param content Comment content (markdown supported)
1326
- * @param filePath Optional file path for inline comment
1327
- * @param lineNumber Optional line number (right side) for inline comment
1328
- * @param status Thread status: active, fixed, wontFix, closed, byDesign, pending (default: active)
1329
- * @returns Created thread
1330
- */
1331
- async addPullRequestThread(project, repositoryId, pullRequestId, content, filePath, lineNumber, status = 'active') {
1332
- this.validateProject(project);
1333
- if (!this.config.enablePullRequestWrite) {
1334
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1335
- }
1336
- const threadData = {
1337
- comments: [
1338
- {
1339
- parentCommentId: 0,
1340
- content: content,
1341
- commentType: 1 // 1 = text
1342
- }
1343
- ],
1344
- status: status
1345
- };
1346
- // Add file context for inline comments
1347
- if (filePath && lineNumber) {
1348
- threadData.threadContext = {
1349
- filePath: filePath.startsWith('/') ? filePath : `/${filePath}`,
1350
- rightFileStart: { line: lineNumber, offset: 1 },
1351
- rightFileEnd: { line: lineNumber, offset: 1 }
1352
- };
1353
- }
1354
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/threads?api-version=${this.apiVersion}`, 'POST', threadData);
1355
- return {
1356
- threadId: response.id,
1357
- status: response.status,
1358
- publishedDate: response.publishedDate,
1359
- filePath: response.threadContext?.filePath,
1360
- comments: response.comments?.map((c) => ({
1361
- id: c.id,
1362
- content: c.content,
1363
- author: c.author?.displayName
1364
- })),
1365
- message: filePath
1366
- ? `Inline comment added to ${filePath} at line ${lineNumber}`
1367
- : 'Comment thread created',
1368
- pullRequestId,
1369
- project
1370
- };
1371
- }
1372
- /**
1373
- * Create a new pull request
1374
- */
1375
- async createPullRequest(project, repositoryId, sourceRefName, targetRefName, title, description, reviewerIds, isDraft) {
1376
- this.validateProject(project);
1377
- if (!this.config.enablePullRequestWrite) {
1378
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1379
- }
1380
- const body = {
1381
- sourceRefName,
1382
- targetRefName,
1383
- title,
1384
- description: description || '',
1385
- isDraft: isDraft || false,
1386
- };
1387
- if (reviewerIds && reviewerIds.length > 0) {
1388
- body.reviewers = reviewerIds.map(id => ({ id }));
1389
- }
1390
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests?api-version=${this.apiVersion}`, 'POST', body, false, { 'Content-Type': 'application/json' });
1391
- return {
1392
- pullRequestId: response.pullRequestId,
1393
- title: response.title,
1394
- status: response.status,
1395
- isDraft: response.isDraft,
1396
- sourceBranch: response.sourceRefName?.replace('refs/heads/', ''),
1397
- targetBranch: response.targetRefName?.replace('refs/heads/', ''),
1398
- createdBy: response.createdBy?.displayName,
1399
- creationDate: response.creationDate,
1400
- url: response.url,
1401
- project
1402
- };
1403
- }
1404
- /**
1405
- * Update an existing pull request (title, description, status, draft)
1406
- */
1407
- async updatePullRequest(project, repositoryId, pullRequestId, updates) {
1408
- this.validateProject(project);
1409
- if (!this.config.enablePullRequestWrite) {
1410
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1411
- }
1412
- const body = {};
1413
- if (updates.title !== undefined)
1414
- body.title = updates.title;
1415
- if (updates.description !== undefined)
1416
- body.description = updates.description;
1417
- if (updates.status !== undefined)
1418
- body.status = updates.status;
1419
- if (updates.isDraft !== undefined)
1420
- body.isDraft = updates.isDraft;
1421
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}?api-version=${this.apiVersion}`, 'PATCH', body, false, { 'Content-Type': 'application/json' });
1422
- return {
1423
- pullRequestId: response.pullRequestId,
1424
- title: response.title,
1425
- description: response.description ? this.truncate(response.description, 200) : null,
1426
- status: response.status,
1427
- isDraft: response.isDraft,
1428
- project
1429
- };
1430
- }
1431
- /**
1432
- * Complete (merge) a pull request
1433
- */
1434
- async completePullRequest(project, repositoryId, pullRequestId, mergeStrategy = 'squash', deleteSourceBranch = true, transitionWorkItems = true, mergeCommitMessage) {
1435
- this.validateProject(project);
1436
- if (!this.config.enablePullRequestWrite) {
1437
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1438
- }
1439
- // Fetch current PR to get lastMergeSourceCommit
1440
- const currentPr = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}?api-version=${this.apiVersion}`);
1441
- const mergeStrategyMap = {
1442
- noFastForward: 1,
1443
- squash: 2,
1444
- rebase: 3,
1445
- rebaseMerge: 4,
1446
- };
1447
- const body = {
1448
- status: 'completed',
1449
- lastMergeSourceCommit: currentPr.lastMergeSourceCommit,
1450
- completionOptions: {
1451
- mergeStrategy: mergeStrategyMap[mergeStrategy],
1452
- deleteSourceBranch,
1453
- transitionWorkItems,
1454
- },
1455
- };
1456
- if (mergeCommitMessage) {
1457
- body.completionOptions.mergeCommitMessage = mergeCommitMessage;
1458
- }
1459
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}?api-version=${this.apiVersion}`, 'PATCH', body, false, { 'Content-Type': 'application/json' });
1460
- return {
1461
- pullRequestId: response.pullRequestId,
1462
- title: response.title,
1463
- status: response.status,
1464
- mergeStrategy,
1465
- closedDate: response.closedDate,
1466
- project
1467
- };
1468
- }
1469
- /**
1470
- * Add or remove a reviewer from a pull request
1471
- */
1472
- async addOrRemovePrReviewer(project, repositoryId, pullRequestId, reviewerId, isRequired, remove) {
1473
- this.validateProject(project);
1474
- if (!this.config.enablePullRequestWrite) {
1475
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1476
- }
1477
- const url = `${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/reviewers/${reviewerId}?api-version=${this.apiVersion}`;
1478
- if (remove) {
1479
- await this.makeRequest(url, 'DELETE');
1480
- return { pullRequestId, reviewerId, action: 'removed', project };
1481
- }
1482
- const body = { id: reviewerId };
1483
- if (isRequired !== undefined) {
1484
- body.isRequired = isRequired;
1485
- }
1486
- const response = await this.makeRequest(url, 'PUT', body, false, { 'Content-Type': 'application/json' });
1487
- return {
1488
- pullRequestId,
1489
- reviewerId: response.id,
1490
- displayName: response.displayName,
1491
- isRequired: response.isRequired,
1492
- vote: response.vote,
1493
- action: 'added',
1494
- project
1495
- };
1496
- }
1497
- /**
1498
- * Submit a vote on a pull request
1499
- */
1500
- async votePullRequest(project, repositoryId, pullRequestId, vote, reviewerId) {
1501
- this.validateProject(project);
1502
- if (!this.config.enablePullRequestWrite) {
1503
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1504
- }
1505
- const voteMap = {
1506
- approve: 10,
1507
- approveWithSuggestions: 5,
1508
- noResponse: 0,
1509
- waitForAuthor: -5,
1510
- reject: -10,
1511
- };
1512
- // If no reviewerId, resolve authenticated user
1513
- let userId = reviewerId;
1514
- if (!userId) {
1515
- const connectionData = await this.makeRequest('_apis/connectionData');
1516
- userId = connectionData.authenticatedUser?.id;
1517
- if (!userId) {
1518
- throw new Error('Could not resolve authenticated user ID. Provide reviewerId explicitly.');
1519
- }
1520
- }
1521
- const body = { vote: voteMap[vote] };
1522
- const response = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/reviewers/${userId}?api-version=${this.apiVersion}`, 'PUT', body, false, { 'Content-Type': 'application/json' });
1523
- return {
1524
- pullRequestId,
1525
- reviewerId: response.id,
1526
- displayName: response.displayName,
1527
- vote: response.vote,
1528
- voteLabel: vote,
1529
- project
1530
- };
1531
- }
1532
- /**
1533
- * Reply to a PR thread and optionally update thread status
1534
- */
1535
- async replyToPrThread(project, repositoryId, pullRequestId, threadId, content, status) {
1536
- this.validateProject(project);
1537
- if (!this.config.enablePullRequestWrite) {
1538
- throw new Error('Pull request write operations are disabled. Set AZUREDEVOPS_ENABLE_PR_WRITE=true to enable.');
1539
- }
1540
- const results = { pullRequestId, threadId, project };
1541
- // Add reply comment if content provided
1542
- if (content) {
1543
- const commentResponse = await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/threads/${threadId}/comments?api-version=${this.apiVersion}`, 'POST', { content, parentCommentId: 0, commentType: 1 }, false, { 'Content-Type': 'application/json' });
1544
- results.comment = {
1545
- id: commentResponse.id,
1546
- content: commentResponse.content,
1547
- author: commentResponse.author?.displayName,
1548
- };
1549
- }
1550
- // Update thread status if provided
1551
- if (status) {
1552
- await this.makeRequest(`${project}/_apis/git/repositories/${repositoryId}/pullrequests/${pullRequestId}/threads/${threadId}?api-version=${this.apiVersion}`, 'PATCH', { status }, false, { 'Content-Type': 'application/json' });
1553
- results.status = status;
1554
- }
1555
- return results;
1556
- }
1557
- }
1558
- //# sourceMappingURL=AzureDevOpsService.js.map