@mcp-consultant-tools/azure-devops-admin 27.0.0-beta.2 → 27.0.0-beta.20

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.
@@ -2,6 +2,7 @@ import axios from 'axios';
2
2
  export class AzureDevOpsAdminService {
3
3
  config;
4
4
  baseUrl;
5
+ feedsUrl;
5
6
  authHeader;
6
7
  apiVersion;
7
8
  constructor(config) {
@@ -18,8 +19,11 @@ export class AzureDevOpsAdminService {
18
19
  enableAgentPoolDisable: config.enableAgentPoolDisable ?? false,
19
20
  enableEnvironmentUpsert: config.enableEnvironmentUpsert ?? false,
20
21
  enableEnvironmentDelete: config.enableEnvironmentDelete ?? false,
22
+ enableClassificationNodeUpsert: config.enableClassificationNodeUpsert ?? false,
23
+ enableClassificationNodeDelete: config.enableClassificationNodeDelete ?? false,
21
24
  };
22
25
  this.baseUrl = `https://dev.azure.com/${this.config.organization}`;
26
+ this.feedsUrl = `https://feeds.dev.azure.com/${this.config.organization}`;
23
27
  this.apiVersion = this.config.apiVersion;
24
28
  this.authHeader = `Basic ${Buffer.from(`:${this.config.pat}`).toString('base64')}`;
25
29
  }
@@ -28,15 +32,38 @@ export class AzureDevOpsAdminService {
28
32
  throw new Error(`Project '${project}' is not in the allowed projects list. Allowed projects: ${this.config.projects.join(', ')}`);
29
33
  }
30
34
  }
31
- async makeRequest(endpoint, method = 'GET', data, customHeaders) {
35
+ validateFeed(feedName) {
36
+ if (this.config.feeds && this.config.feeds.length > 0
37
+ && !this.config.feeds.includes(feedName)) {
38
+ throw new Error(`Feed '${feedName}' is not in the allowed feeds list. ` +
39
+ `Allowed feeds: ${this.config.feeds.join(', ')}`);
40
+ }
41
+ }
42
+ /**
43
+ * Converts a date string to ISO 8601 format with time component.
44
+ * Azure DevOps Classification Nodes API requires full ISO 8601 format.
45
+ * Input: "2026-02-16" → Output: "2026-02-16T00:00:00Z"
46
+ */
47
+ formatDateForAdo(dateStr) {
48
+ // If already has time component, return as-is
49
+ if (dateStr.includes('T')) {
50
+ return dateStr;
51
+ }
52
+ // Add time component (midnight UTC)
53
+ return `${dateStr}T00:00:00Z`;
54
+ }
55
+ async makeRequest(endpoint, method = 'GET', data, customHeaders, contentType, baseUrlOverride) {
32
56
  try {
33
- const url = `${this.baseUrl}/${endpoint}`;
57
+ const url = `${baseUrlOverride || this.baseUrl}/${endpoint}`;
58
+ // Determine content type: explicit override > default based on method
59
+ // Note: Most PATCH endpoints use json-patch+json, but some (like classification nodes) use regular json
60
+ const defaultContentType = method === 'PATCH' ? 'application/json-patch+json' : 'application/json';
34
61
  const response = await axios({
35
62
  method,
36
63
  url,
37
64
  headers: {
38
65
  'Authorization': this.authHeader,
39
- 'Content-Type': method === 'PATCH' ? 'application/json-patch+json' : 'application/json',
66
+ 'Content-Type': contentType || defaultContentType,
40
67
  'Accept': 'application/json',
41
68
  ...customHeaders
42
69
  },
@@ -57,7 +84,8 @@ export class AzureDevOpsAdminService {
57
84
  throw new Error('Azure DevOps authentication failed. Please check your PAT token and permissions.');
58
85
  }
59
86
  if (error.response?.status === 403) {
60
- throw new Error('Azure DevOps access denied. Please check your PAT scopes and project permissions.');
87
+ const detail = typeof errorDetails === 'string' ? errorDetails : '';
88
+ throw new Error(`Azure DevOps access denied: ${detail || 'Please check your PAT scopes and project permissions.'}`);
61
89
  }
62
90
  if (error.response?.status === 404) {
63
91
  throw new Error(`Azure DevOps resource not found: ${endpoint}`);
@@ -320,15 +348,63 @@ export class AzureDevOpsAdminService {
320
348
  records: mappedRecords
321
349
  };
322
350
  }
323
- async getBuildLogs(project, buildId, logId) {
351
+ /**
352
+ * Filter log content to reduce noise from progress indicators
353
+ * @param content Raw log content string
354
+ * @param mode Filter mode: 'summary' removes progress, 'errors' shows only errors, 'full' returns everything
355
+ */
356
+ filterLogContent(content, mode) {
357
+ const lines = content.split('\n');
358
+ const originalLineCount = lines.length;
359
+ if (mode === 'full') {
360
+ return { filtered: content, originalLineCount, filteredLineCount: originalLineCount };
361
+ }
362
+ // Progress indicator patterns to remove in summary mode
363
+ const PROGRESS_PATTERNS = [
364
+ /remote: Counting objects:\s+\d+%/,
365
+ /remote: Compressing objects:\s+\d+%/,
366
+ /Receiving objects:\s+\d+%/,
367
+ /Resolving deltas:\s+\d+%/,
368
+ /Unpacking objects:\s+\d+%/,
369
+ /Updating files:\s+\d+%/,
370
+ ];
371
+ // Error/warning patterns for errors mode
372
+ const ERROR_PATTERNS = [
373
+ /##\[error\]/i,
374
+ /##\[warning\]/i,
375
+ /\berror\b.*:/i,
376
+ /\bfailed\b/i,
377
+ /\bexception\b/i,
378
+ /\bfatal\b/i,
379
+ ];
380
+ const filteredLines = lines.filter(line => {
381
+ // Remove timestamp prefix for pattern matching
382
+ const trimmedLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, '');
383
+ if (mode === 'errors') {
384
+ return ERROR_PATTERNS.some(p => p.test(trimmedLine));
385
+ }
386
+ // summary mode: exclude progress indicators, keep everything else
387
+ return !PROGRESS_PATTERNS.some(p => p.test(trimmedLine));
388
+ });
389
+ return {
390
+ filtered: filteredLines.join('\n'),
391
+ originalLineCount,
392
+ filteredLineCount: filteredLines.length
393
+ };
394
+ }
395
+ async getBuildLogs(project, buildId, logId, mode = 'summary') {
324
396
  this.validateProject(project);
325
397
  if (logId !== undefined) {
326
398
  const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/logs/${logId}?api-version=${this.apiVersion}`);
399
+ const { filtered, originalLineCount, filteredLineCount } = this.filterLogContent(response, mode);
327
400
  return {
328
401
  buildId,
329
402
  logId,
330
403
  project,
331
- content: response
404
+ mode,
405
+ originalLineCount,
406
+ filteredLineCount,
407
+ content: filtered
332
408
  };
333
409
  }
334
410
  const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/logs?api-version=${this.apiVersion}`);
@@ -346,11 +422,46 @@ export class AzureDevOpsAdminService {
346
422
  }))
347
423
  };
348
424
  }
349
- async createPipelineDefinition(project, name, repositoryId, yamlPath, folder) {
425
+ async createPipelineDefinition(project, name, repositoryId, yamlPath, folder, repositoryType = 'TfsGit', repositoryUrl, defaultBranch, serviceConnectionId) {
350
426
  this.validateProject(project);
351
427
  if (!this.config.enablePipelineUpsert) {
352
428
  throw new Error('Pipeline upsert operations are disabled. Set AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true to enable.');
353
429
  }
430
+ // Validation for external repositories
431
+ if (repositoryType !== 'TfsGit') {
432
+ if (!repositoryUrl) {
433
+ throw new Error(`repositoryUrl is required for ${repositoryType} repositories`);
434
+ }
435
+ if (!serviceConnectionId) {
436
+ throw new Error(`serviceConnectionId is required for ${repositoryType} repositories`);
437
+ }
438
+ }
439
+ // Normalize defaultBranch format
440
+ const normalizedBranch = defaultBranch
441
+ ? (defaultBranch.startsWith('refs/heads/') ? defaultBranch : `refs/heads/${defaultBranch}`)
442
+ : 'refs/heads/main';
443
+ // Build repository object based on type
444
+ let repository;
445
+ if (repositoryType === 'TfsGit') {
446
+ // Azure Repos - simple format
447
+ repository = {
448
+ id: repositoryId,
449
+ type: 'TfsGit'
450
+ };
451
+ }
452
+ else {
453
+ // GitHub / GitHubEnterprise - extended format
454
+ repository = {
455
+ id: repositoryId,
456
+ name: repositoryId, // Same as id for GitHub
457
+ type: repositoryType,
458
+ url: repositoryUrl,
459
+ defaultBranch: normalizedBranch,
460
+ properties: {
461
+ connectedServiceId: serviceConnectionId
462
+ }
463
+ };
464
+ }
354
465
  const definition = {
355
466
  name,
356
467
  path: folder || '\\',
@@ -360,10 +471,7 @@ export class AzureDevOpsAdminService {
360
471
  type: 2,
361
472
  yamlFilename: yamlPath
362
473
  },
363
- repository: {
364
- id: repositoryId,
365
- type: 'TfsGit'
366
- }
474
+ repository
367
475
  };
368
476
  const response = await this.makeRequest(`${project}/_apis/build/definitions?api-version=${this.apiVersion}`, 'POST', definition);
369
477
  return {
@@ -372,6 +480,7 @@ export class AzureDevOpsAdminService {
372
480
  path: response.path,
373
481
  revision: response.revision,
374
482
  project,
483
+ repositoryType,
375
484
  url: response._links?.web?.href
376
485
  };
377
486
  }
@@ -463,6 +572,68 @@ export class AzureDevOpsAdminService {
463
572
  };
464
573
  }
465
574
  // ═══════════════════════════════════════════════════════════════════════════════
575
+ // PIPELINE APPROVAL OPERATIONS
576
+ // ═══════════════════════════════════════════════════════════════════════════════
577
+ async listPendingApprovals(project, buildId) {
578
+ this.validateProject(project);
579
+ // Step 1: Fetch build timeline to find Checkpoint.Approval records
580
+ const timeline = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/timeline?api-version=${this.apiVersion}`);
581
+ const approvalRecords = (timeline.records || []).filter((r) => r.type === 'Checkpoint.Approval');
582
+ if (approvalRecords.length === 0) {
583
+ return {
584
+ project,
585
+ buildId,
586
+ totalCount: 0,
587
+ approvals: [],
588
+ message: 'No approval checkpoints found for this build'
589
+ };
590
+ }
591
+ // Step 2: Extract approval IDs from record identifiers
592
+ const approvalIds = approvalRecords.map((r) => r.id);
593
+ // Step 3: Query the approvals API with those IDs
594
+ const approvalIdsParam = approvalIds.join(',');
595
+ const approvalsResponse = await this.makeRequest(`${project}/_apis/pipelines/approvals?approvalIds=${approvalIdsParam}&$expand=steps&api-version=${this.apiVersion}`);
596
+ const approvals = (approvalsResponse.value || [approvalsResponse]).map((a) => ({
597
+ id: a.id,
598
+ status: a.status,
599
+ createdOn: a.createdOn,
600
+ executionOrder: a.executionOrder,
601
+ minRequiredApprovers: a.minRequiredApprovers,
602
+ instructions: a.instructions,
603
+ blockedApprovers: a.blockedApprovers,
604
+ steps: (a.steps || []).map((s) => ({
605
+ assignedApprover: s.assignedApprover?.displayName,
606
+ assignedApproverId: s.assignedApprover?.uniqueName,
607
+ status: s.status,
608
+ comment: s.comment,
609
+ initiatedOn: s.initiatedOn,
610
+ lastModifiedOn: s.lastModifiedOn
611
+ }))
612
+ }));
613
+ return {
614
+ project,
615
+ buildId,
616
+ totalCount: approvals.length,
617
+ approvals
618
+ };
619
+ }
620
+ async approveStage(project, approvalId, status, comment) {
621
+ this.validateProject(project);
622
+ if (!this.config.enablePipelineUpsert) {
623
+ throw new Error('Pipeline upsert operations are disabled. Set AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true to enable.');
624
+ }
625
+ const body = [{ approvalId, status, comment: comment || '' }];
626
+ const response = await this.makeRequest(`${project}/_apis/pipelines/approvals?api-version=${this.apiVersion}`, 'PATCH', body, { 'Content-Type': 'application/json' });
627
+ const result = Array.isArray(response.value) ? response.value[0] : response;
628
+ return {
629
+ id: result.id,
630
+ status: result.status,
631
+ comment,
632
+ project,
633
+ message: `Approval ${approvalId} ${status}`
634
+ };
635
+ }
636
+ // ═══════════════════════════════════════════════════════════════════════════════
466
637
  // SERVICE CONNECTION OPERATIONS
467
638
  // ═══════════════════════════════════════════════════════════════════════════════
468
639
  async listServiceConnections(project) {
@@ -689,7 +860,7 @@ export class AzureDevOpsAdminService {
689
860
  name: name
690
861
  }]
691
862
  };
692
- const response = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups?api-version=${this.apiVersion}`, 'POST', variableGroup);
863
+ const response = await this.makeRequest(`_apis/distributedtask/variablegroups?api-version=${this.apiVersion}`, 'POST', variableGroup);
693
864
  return {
694
865
  id: response.id,
695
866
  name: response.name,
@@ -708,7 +879,7 @@ export class AzureDevOpsAdminService {
708
879
  name: updates.name || current.name,
709
880
  description: updates.description || current.description
710
881
  };
711
- const response = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', updated);
882
+ const response = await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', updated);
712
883
  return {
713
884
  id: response.id,
714
885
  name: response.name,
@@ -723,7 +894,7 @@ export class AzureDevOpsAdminService {
723
894
  }
724
895
  const current = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`);
725
896
  current.variables[variableName] = { value, isSecret };
726
- const response = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', current);
897
+ const response = await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', current);
727
898
  return {
728
899
  id: response.id,
729
900
  name: response.name,
@@ -744,7 +915,7 @@ export class AzureDevOpsAdminService {
744
915
  else {
745
916
  throw new Error(`Variable '${variableName}' not found in group ${groupId}`);
746
917
  }
747
- const response = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', current);
918
+ const response = await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', current);
748
919
  return {
749
920
  id: response.id,
750
921
  name: response.name,
@@ -757,7 +928,7 @@ export class AzureDevOpsAdminService {
757
928
  if (!this.config.enableVariableGroupDelete) {
758
929
  throw new Error('Variable group delete operations are disabled. Set AZUREDEVOPS_ENABLE_VARIABLE_GROUP_DELETE=true to enable.');
759
930
  }
760
- await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'DELETE');
931
+ await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'DELETE');
761
932
  return {
762
933
  groupId,
763
934
  project,
@@ -1061,5 +1232,263 @@ export class AzureDevOpsAdminService {
1061
1232
  deleted: true
1062
1233
  };
1063
1234
  }
1235
+ // ═══════════════════════════════════════════════════════════════════════════════
1236
+ // CLASSIFICATION NODE OPERATIONS (Iterations & Areas)
1237
+ // ═══════════════════════════════════════════════════════════════════════════════
1238
+ /**
1239
+ * Flatten classification node hierarchy into a list
1240
+ */
1241
+ flattenClassificationNodes(node, parentPath = '') {
1242
+ const result = [];
1243
+ const currentPath = parentPath ? `${parentPath}\\${node.name}` : node.name;
1244
+ result.push({
1245
+ id: node.id,
1246
+ identifier: node.identifier,
1247
+ name: node.name,
1248
+ path: currentPath,
1249
+ structureType: node.structureType,
1250
+ hasChildren: node.hasChildren || false,
1251
+ attributes: node.attributes || {},
1252
+ url: node.url
1253
+ });
1254
+ if (node.children && Array.isArray(node.children)) {
1255
+ for (const child of node.children) {
1256
+ result.push(...this.flattenClassificationNodes(child, currentPath));
1257
+ }
1258
+ }
1259
+ return result;
1260
+ }
1261
+ async listClassificationNodes(project, structureType, depth = 10) {
1262
+ this.validateProject(project);
1263
+ const response = await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}?$depth=${depth}&api-version=${this.apiVersion}`);
1264
+ const nodes = this.flattenClassificationNodes(response);
1265
+ return {
1266
+ project,
1267
+ structureType,
1268
+ totalCount: nodes.length,
1269
+ nodes: nodes.map((node) => ({
1270
+ id: node.id,
1271
+ identifier: node.identifier,
1272
+ name: node.name,
1273
+ path: node.path,
1274
+ hasChildren: node.hasChildren,
1275
+ ...(structureType === 'iterations' && node.attributes ? {
1276
+ startDate: node.attributes.startDate,
1277
+ finishDate: node.attributes.finishDate,
1278
+ timeFrame: node.attributes.timeFrame
1279
+ } : {})
1280
+ }))
1281
+ };
1282
+ }
1283
+ async getClassificationNode(project, structureType, path) {
1284
+ this.validateProject(project);
1285
+ // Path should be URL encoded and without leading backslash
1286
+ const cleanPath = path.replace(/^\\/, '').replace(/\\/g, '/');
1287
+ const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
1288
+ const response = await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}?api-version=${this.apiVersion}`);
1289
+ return {
1290
+ id: response.id,
1291
+ identifier: response.identifier,
1292
+ name: response.name,
1293
+ structureType: response.structureType,
1294
+ hasChildren: response.hasChildren || false,
1295
+ path: response.path,
1296
+ project,
1297
+ ...(structureType === 'iterations' && response.attributes ? {
1298
+ startDate: response.attributes.startDate,
1299
+ finishDate: response.attributes.finishDate,
1300
+ timeFrame: response.attributes.timeFrame
1301
+ } : {}),
1302
+ url: response.url
1303
+ };
1304
+ }
1305
+ async createClassificationNode(project, structureType, name, parentPath, attributes) {
1306
+ this.validateProject(project);
1307
+ if (!this.config.enableClassificationNodeUpsert) {
1308
+ throw new Error('Classification node upsert operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_UPSERT=true to enable.');
1309
+ }
1310
+ const body = { name };
1311
+ if (structureType === 'iterations' && attributes) {
1312
+ body.attributes = {};
1313
+ // Azure DevOps requires ISO 8601 format with time component
1314
+ if (attributes.startDate)
1315
+ body.attributes.startDate = this.formatDateForAdo(attributes.startDate);
1316
+ if (attributes.finishDate)
1317
+ body.attributes.finishDate = this.formatDateForAdo(attributes.finishDate);
1318
+ }
1319
+ // Build the endpoint - if parentPath is provided, create under that path
1320
+ let endpoint = `${project}/_apis/wit/classificationnodes/${structureType}`;
1321
+ if (parentPath) {
1322
+ const cleanPath = parentPath.replace(/^\\/, '').replace(/\\/g, '/');
1323
+ const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
1324
+ endpoint = `${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}`;
1325
+ }
1326
+ const response = await this.makeRequest(`${endpoint}?api-version=${this.apiVersion}`, 'POST', body);
1327
+ return {
1328
+ id: response.id,
1329
+ identifier: response.identifier,
1330
+ name: response.name,
1331
+ path: response.path,
1332
+ structureType,
1333
+ project,
1334
+ ...(structureType === 'iterations' && response.attributes ? {
1335
+ startDate: response.attributes.startDate,
1336
+ finishDate: response.attributes.finishDate
1337
+ } : {}),
1338
+ url: response.url
1339
+ };
1340
+ }
1341
+ async addIterationToTeam(project, team, iterationId) {
1342
+ this.validateProject(project);
1343
+ if (!this.config.enableClassificationNodeUpsert) {
1344
+ throw new Error('Classification node upsert operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_UPSERT=true to enable.');
1345
+ }
1346
+ const encodedTeam = encodeURIComponent(team);
1347
+ const endpoint = `${project}/${encodedTeam}/_apis/work/teamsettings/iterations?api-version=${this.apiVersion}`;
1348
+ const response = await this.makeRequest(endpoint, 'POST', { id: iterationId });
1349
+ return {
1350
+ id: response.id,
1351
+ name: response.name,
1352
+ path: response.path,
1353
+ url: response.url,
1354
+ attributes: response.attributes ? {
1355
+ startDate: response.attributes.startDate,
1356
+ finishDate: response.attributes.finishDate,
1357
+ timeFrame: response.attributes.timeFrame,
1358
+ } : undefined,
1359
+ };
1360
+ }
1361
+ async updateClassificationNode(project, structureType, path, updates) {
1362
+ this.validateProject(project);
1363
+ if (!this.config.enableClassificationNodeUpsert) {
1364
+ throw new Error('Classification node upsert operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_UPSERT=true to enable.');
1365
+ }
1366
+ const cleanPath = path.replace(/^\\/, '').replace(/\\/g, '/');
1367
+ const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
1368
+ const body = {};
1369
+ if (updates.name)
1370
+ body.name = updates.name;
1371
+ if (structureType === 'iterations') {
1372
+ if (updates.startDate !== undefined || updates.finishDate !== undefined) {
1373
+ body.attributes = {};
1374
+ // Azure DevOps requires ISO 8601 format with time component
1375
+ if (updates.startDate !== undefined)
1376
+ body.attributes.startDate = this.formatDateForAdo(updates.startDate);
1377
+ if (updates.finishDate !== undefined)
1378
+ body.attributes.finishDate = this.formatDateForAdo(updates.finishDate);
1379
+ }
1380
+ }
1381
+ // Classification Nodes API uses regular JSON, not JSON Patch format
1382
+ const response = await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}?api-version=${this.apiVersion}`, 'PATCH', body, undefined, 'application/json' // Override default json-patch+json content type
1383
+ );
1384
+ return {
1385
+ id: response.id,
1386
+ identifier: response.identifier,
1387
+ name: response.name,
1388
+ path: response.path,
1389
+ structureType,
1390
+ project,
1391
+ ...(structureType === 'iterations' && response.attributes ? {
1392
+ startDate: response.attributes.startDate,
1393
+ finishDate: response.attributes.finishDate
1394
+ } : {}),
1395
+ url: response.url
1396
+ };
1397
+ }
1398
+ async deleteClassificationNode(project, structureType, path, reclassifyId) {
1399
+ this.validateProject(project);
1400
+ if (!this.config.enableClassificationNodeDelete) {
1401
+ throw new Error('Classification node delete operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_DELETE=true to enable.');
1402
+ }
1403
+ const cleanPath = path.replace(/^\\/, '').replace(/\\/g, '/');
1404
+ const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
1405
+ await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}?$reclassifyId=${reclassifyId}&api-version=${this.apiVersion}`, 'DELETE');
1406
+ return {
1407
+ path,
1408
+ structureType,
1409
+ project,
1410
+ reclassifyId,
1411
+ deleted: true
1412
+ };
1413
+ }
1414
+ // ========================================
1415
+ // ARTIFACT FEED METHODS
1416
+ // ========================================
1417
+ async listFeedPackages(feedName, options) {
1418
+ this.validateFeed(feedName);
1419
+ if (options?.project) {
1420
+ this.validateProject(options.project);
1421
+ }
1422
+ const projectPrefix = options?.project ? `${options.project}/` : '';
1423
+ const queryParams = new URLSearchParams();
1424
+ queryParams.append('api-version', this.apiVersion);
1425
+ if (options?.namePrefix)
1426
+ queryParams.append('packageNameQuery', options.namePrefix);
1427
+ if (options?.packageType)
1428
+ queryParams.append('protocolType', options.packageType);
1429
+ if (options?.top)
1430
+ queryParams.append('$top', String(options.top));
1431
+ const endpoint = `${projectPrefix}_apis/packaging/Feeds/${encodeURIComponent(feedName)}/packages?${queryParams.toString()}`;
1432
+ const response = await this.makeRequest(endpoint, 'GET', undefined, undefined, undefined, this.feedsUrl);
1433
+ return {
1434
+ feed: feedName,
1435
+ project: options?.project || '(org-scoped)',
1436
+ totalCount: response.value.length,
1437
+ packages: response.value.map((pkg) => ({
1438
+ id: pkg.id,
1439
+ name: pkg.name,
1440
+ protocolType: pkg.protocolType,
1441
+ latestVersion: pkg.versions?.[0]?.version,
1442
+ publishDate: pkg.versions?.[0]?.publishDate,
1443
+ description: pkg.description,
1444
+ })),
1445
+ };
1446
+ }
1447
+ async getPackageVersions(feedName, packageName, options) {
1448
+ this.validateFeed(feedName);
1449
+ if (options?.project) {
1450
+ this.validateProject(options.project);
1451
+ }
1452
+ // Step 1: Resolve package name → package ID
1453
+ const projectPrefix = options?.project ? `${options.project}/` : '';
1454
+ const listParams = new URLSearchParams();
1455
+ listParams.append('api-version', this.apiVersion);
1456
+ listParams.append('packageNameQuery', packageName);
1457
+ if (options?.packageType)
1458
+ listParams.append('protocolType', options.packageType);
1459
+ const listEndpoint = `${projectPrefix}_apis/packaging/Feeds/${encodeURIComponent(feedName)}/packages?${listParams.toString()}`;
1460
+ const listResponse = await this.makeRequest(listEndpoint, 'GET', undefined, undefined, undefined, this.feedsUrl);
1461
+ const exactMatch = listResponse.value.find((pkg) => pkg.name.toLowerCase() === packageName.toLowerCase());
1462
+ if (!exactMatch) {
1463
+ const available = listResponse.value.map((pkg) => pkg.name).slice(0, 10);
1464
+ throw new Error(`Package '${packageName}' not found in feed '${feedName}'. ` +
1465
+ (available.length > 0
1466
+ ? `Similar packages: ${available.join(', ')}`
1467
+ : 'No matching packages found.'));
1468
+ }
1469
+ // Step 2: Get versions for the resolved package ID
1470
+ const versionParams = new URLSearchParams();
1471
+ versionParams.append('api-version', this.apiVersion);
1472
+ if (options?.top)
1473
+ versionParams.append('$top', String(options.top));
1474
+ if (options?.includeDelisted)
1475
+ versionParams.append('includeDelisted', 'true');
1476
+ const versionEndpoint = `${projectPrefix}_apis/packaging/Feeds/${encodeURIComponent(feedName)}/packages/${exactMatch.id}/versions?${versionParams.toString()}`;
1477
+ const versionResponse = await this.makeRequest(versionEndpoint, 'GET', undefined, undefined, undefined, this.feedsUrl);
1478
+ return {
1479
+ feed: feedName,
1480
+ project: options?.project || '(org-scoped)',
1481
+ packageName: exactMatch.name,
1482
+ packageId: exactMatch.id,
1483
+ protocolType: exactMatch.protocolType,
1484
+ versions: versionResponse.value.map((v) => ({
1485
+ version: v.version,
1486
+ publishDate: v.publishDate,
1487
+ isLatest: v.isLatest ?? false,
1488
+ isListed: v.isListed ?? true,
1489
+ description: v.description,
1490
+ })),
1491
+ };
1492
+ }
1064
1493
  }
1065
1494
  //# sourceMappingURL=AzureDevOpsAdminService.js.map