specweave 0.13.6 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +189 -0
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/status-line.d.ts +14 -0
- package/dist/cli/commands/status-line.d.ts.map +1 -0
- package/dist/cli/commands/status-line.js +75 -0
- package/dist/cli/commands/status-line.js.map +1 -0
- package/dist/core/status-line/status-line-manager.d.ts +62 -0
- package/dist/core/status-line/status-line-manager.d.ts.map +1 -0
- package/dist/core/status-line/status-line-manager.js +169 -0
- package/dist/core/status-line/status-line-manager.js.map +1 -0
- package/dist/core/status-line/types.d.ts +50 -0
- package/dist/core/status-line/types.d.ts.map +1 -0
- package/dist/core/status-line/types.js +17 -0
- package/dist/core/status-line/types.js.map +1 -0
- package/dist/utils/project-mapper.d.ts +74 -0
- package/dist/utils/project-mapper.d.ts.map +1 -0
- package/dist/utils/project-mapper.js +273 -0
- package/dist/utils/project-mapper.js.map +1 -0
- package/dist/utils/spec-splitter.d.ts +68 -0
- package/dist/utils/spec-splitter.d.ts.map +1 -0
- package/dist/utils/spec-splitter.js +314 -0
- package/dist/utils/spec-splitter.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/hooks/lib/update-status-line.sh +138 -0
- package/plugins/specweave/hooks/post-task-completion.sh +10 -0
- package/plugins/specweave/skills/multi-project-spec-mapper/SKILL.md +399 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +453 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.ts +633 -0
- package/plugins/specweave-docs/skills/docusaurus/SKILL.md +17 -3
- package/plugins/specweave-docs-preview/commands/preview.md +29 -4
- package/plugins/specweave-github/lib/github-multi-project-sync.js +340 -0
- package/plugins/specweave-github/lib/github-multi-project-sync.ts +461 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.js +244 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +358 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import {
|
|
3
|
+
getPrimaryProject,
|
|
4
|
+
suggestJiraItemType,
|
|
5
|
+
mapUserStoryToProjects
|
|
6
|
+
} from "../../../src/utils/project-mapper.js";
|
|
7
|
+
import { parseSpecFile } from "../../../src/utils/spec-splitter.js";
|
|
8
|
+
class AdoMultiProjectSync {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.client = axios.create({
|
|
12
|
+
baseURL: `https://dev.azure.com/${config.organization}`,
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json-patch+json",
|
|
15
|
+
"Authorization": `Basic ${Buffer.from(":" + config.pat).toString("base64")}`
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sync spec to ADO projects with intelligent mapping
|
|
21
|
+
*
|
|
22
|
+
* @param specPath Path to spec file
|
|
23
|
+
* @returns Array of sync results
|
|
24
|
+
*/
|
|
25
|
+
async syncSpec(specPath) {
|
|
26
|
+
const results = [];
|
|
27
|
+
const parsedSpec = await parseSpecFile(specPath);
|
|
28
|
+
const isAreaPathBased = !!this.config.project && !!this.config.areaPaths;
|
|
29
|
+
if (isAreaPathBased) {
|
|
30
|
+
results.push(...await this.syncAreaPathBased(parsedSpec));
|
|
31
|
+
} else if (this.config.projects) {
|
|
32
|
+
results.push(...await this.syncMultipleProjects(parsedSpec));
|
|
33
|
+
} else {
|
|
34
|
+
throw new Error("Invalid config: Must specify projects[] or project+areaPaths[]");
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pattern 1: Sync to multiple ADO projects (simple)
|
|
40
|
+
*
|
|
41
|
+
* Each team → separate ADO project
|
|
42
|
+
* - FE user stories → FE-Project
|
|
43
|
+
* - BE user stories → BE-Project
|
|
44
|
+
* - MOBILE user stories → MOBILE-Project
|
|
45
|
+
*/
|
|
46
|
+
async syncMultipleProjects(parsedSpec) {
|
|
47
|
+
const results = [];
|
|
48
|
+
const epicsByProject = /* @__PURE__ */ new Map();
|
|
49
|
+
if (this.config.autoCreateEpics !== false) {
|
|
50
|
+
for (const projectName of this.config.projects) {
|
|
51
|
+
const epicResult = await this.createEpicForProject(parsedSpec, projectName);
|
|
52
|
+
epicsByProject.set(projectName, epicResult.workItemId);
|
|
53
|
+
results.push(epicResult);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const projectStories = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const userStory of parsedSpec.userStories) {
|
|
58
|
+
if (this.config.intelligentMapping !== false) {
|
|
59
|
+
const mappings = mapUserStoryToProjects(userStory);
|
|
60
|
+
if (mappings.length > 0 && mappings[0].confidence >= 0.3) {
|
|
61
|
+
const primary = mappings[0];
|
|
62
|
+
const projectName = this.findProjectForId(primary.projectId);
|
|
63
|
+
if (projectName) {
|
|
64
|
+
const existing = projectStories.get(projectName) || [];
|
|
65
|
+
existing.push({ story: userStory, confidence: primary.confidence });
|
|
66
|
+
projectStories.set(projectName, existing);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
console.warn(`\u26A0\uFE0F Low confidence for ${userStory.id} (${(mappings[0]?.confidence || 0) * 100}%) - assigning to ${this.config.projects[0]}`);
|
|
70
|
+
const fallback = this.config.projects[0];
|
|
71
|
+
const existing = projectStories.get(fallback) || [];
|
|
72
|
+
existing.push({ story: userStory, confidence: mappings[0]?.confidence || 0 });
|
|
73
|
+
projectStories.set(fallback, existing);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [projectName, stories] of projectStories.entries()) {
|
|
78
|
+
const epicId = epicsByProject.get(projectName);
|
|
79
|
+
for (const { story, confidence } of stories) {
|
|
80
|
+
const result = await this.createWorkItemForUserStory(projectName, story, epicId, confidence);
|
|
81
|
+
results.push(result);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pattern 2: Sync to single project with area paths (advanced)
|
|
88
|
+
*
|
|
89
|
+
* - Single ADO project with area paths for teams
|
|
90
|
+
* - Epic-level: Root area path
|
|
91
|
+
* - Story-level: Team-specific area paths
|
|
92
|
+
*
|
|
93
|
+
* Example:
|
|
94
|
+
* ADO Project: Shared-Project
|
|
95
|
+
* Epic: User Authentication (Root area path)
|
|
96
|
+
* User Story: Login UI (Area Path: Shared-Project\FE)
|
|
97
|
+
* User Story: Auth API (Area Path: Shared-Project\BE)
|
|
98
|
+
* User Story: Mobile Auth (Area Path: Shared-Project\MOBILE)
|
|
99
|
+
*/
|
|
100
|
+
async syncAreaPathBased(parsedSpec) {
|
|
101
|
+
const results = [];
|
|
102
|
+
if (!this.config.project || !this.config.areaPaths) {
|
|
103
|
+
throw new Error("Area path mode requires project and areaPaths");
|
|
104
|
+
}
|
|
105
|
+
const epicResult = await this.createEpicInRootArea(parsedSpec);
|
|
106
|
+
results.push(epicResult);
|
|
107
|
+
const areaPathStories = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const userStory of parsedSpec.userStories) {
|
|
109
|
+
const primaryProject = getPrimaryProject(userStory);
|
|
110
|
+
if (primaryProject) {
|
|
111
|
+
const areaPath = this.findAreaPathForProjectId(primaryProject.projectId);
|
|
112
|
+
if (areaPath) {
|
|
113
|
+
const existing = areaPathStories.get(areaPath) || [];
|
|
114
|
+
existing.push(userStory);
|
|
115
|
+
areaPathStories.set(areaPath, existing);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [areaPath, stories] of areaPathStories.entries()) {
|
|
120
|
+
for (const story of stories) {
|
|
121
|
+
const result = await this.createWorkItemInAreaPath(areaPath, story, epicResult.workItemId);
|
|
122
|
+
results.push(result);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create epic for project (Pattern 1: Multiple Projects)
|
|
129
|
+
*/
|
|
130
|
+
async createEpicForProject(parsedSpec, projectName) {
|
|
131
|
+
const title = `${parsedSpec.metadata.title} - ${projectName}`;
|
|
132
|
+
const description = `<h2>${projectName} Implementation</h2>
|
|
133
|
+
|
|
134
|
+
<strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
|
|
135
|
+
<strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
|
|
136
|
+
<strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
|
|
137
|
+
|
|
138
|
+
<h3>Executive Summary</h3>
|
|
139
|
+
|
|
140
|
+
${parsedSpec.executiveSummary}
|
|
141
|
+
|
|
142
|
+
<h3>Scope (${projectName})</h3>
|
|
143
|
+
|
|
144
|
+
This epic covers all ${projectName}-related user stories for "${parsedSpec.metadata.title}".
|
|
145
|
+
|
|
146
|
+
User stories will be added as child work items.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
\u{1F916} Auto-generated by SpecWeave
|
|
151
|
+
`;
|
|
152
|
+
const workItem = await this.createWorkItem(projectName, this.config.workItemTypes?.epic || "Epic", {
|
|
153
|
+
"System.Title": title,
|
|
154
|
+
"System.Description": description,
|
|
155
|
+
"System.State": "New"
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
project: projectName,
|
|
159
|
+
workItemId: workItem.id,
|
|
160
|
+
workItemType: "Epic",
|
|
161
|
+
title,
|
|
162
|
+
url: workItem.url,
|
|
163
|
+
action: "created"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create epic in root area path (Pattern 2: Area Paths)
|
|
168
|
+
*/
|
|
169
|
+
async createEpicInRootArea(parsedSpec) {
|
|
170
|
+
const title = parsedSpec.metadata.title;
|
|
171
|
+
const description = `<h2>${parsedSpec.metadata.title}</h2>
|
|
172
|
+
|
|
173
|
+
<strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
|
|
174
|
+
<strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
|
|
175
|
+
<strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
|
|
176
|
+
|
|
177
|
+
<h3>Executive Summary</h3>
|
|
178
|
+
|
|
179
|
+
${parsedSpec.executiveSummary}
|
|
180
|
+
|
|
181
|
+
<h3>User Stories (${parsedSpec.userStories.length} total)</h3>
|
|
182
|
+
|
|
183
|
+
<ul>
|
|
184
|
+
${parsedSpec.userStories.map((s, i) => `<li>${i + 1}. ${s.id}: ${s.title}</li>`).join("\n")}
|
|
185
|
+
</ul>
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
\u{1F916} Auto-generated by SpecWeave
|
|
190
|
+
`;
|
|
191
|
+
const workItem = await this.createWorkItem(this.config.project, this.config.workItemTypes?.epic || "Epic", {
|
|
192
|
+
"System.Title": title,
|
|
193
|
+
"System.Description": description,
|
|
194
|
+
"System.AreaPath": this.config.project,
|
|
195
|
+
// Root area path
|
|
196
|
+
"System.State": "New"
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
project: this.config.project,
|
|
200
|
+
workItemId: workItem.id,
|
|
201
|
+
workItemType: "Epic",
|
|
202
|
+
title,
|
|
203
|
+
url: workItem.url,
|
|
204
|
+
action: "created"
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create work item for user story (Pattern 1: Multiple Projects)
|
|
209
|
+
*/
|
|
210
|
+
async createWorkItemForUserStory(projectName, userStory, epicId, confidence) {
|
|
211
|
+
const title = `${userStory.id}: ${userStory.title}`;
|
|
212
|
+
const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
|
|
213
|
+
const description = `<h3>${userStory.title}</h3>
|
|
214
|
+
|
|
215
|
+
${userStory.description}
|
|
216
|
+
|
|
217
|
+
<h4>Acceptance Criteria</h4>
|
|
218
|
+
|
|
219
|
+
<ul>
|
|
220
|
+
${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join("\n")}
|
|
221
|
+
</ul>
|
|
222
|
+
|
|
223
|
+
${userStory.technicalContext ? `<h4>Technical Context</h4>
|
|
224
|
+
|
|
225
|
+
${userStory.technicalContext}
|
|
226
|
+
` : ""}
|
|
227
|
+
|
|
228
|
+
${confidence !== void 0 ? `<p><em>Classification confidence: ${(confidence * 100).toFixed(0)}%</em></p>
|
|
229
|
+
` : ""}
|
|
230
|
+
|
|
231
|
+
<p>\u{1F916} Auto-generated by SpecWeave</p>
|
|
232
|
+
`;
|
|
233
|
+
const fields = {
|
|
234
|
+
"System.Title": title,
|
|
235
|
+
"System.Description": description,
|
|
236
|
+
"System.State": "New"
|
|
237
|
+
};
|
|
238
|
+
const workItem = await this.createWorkItem(projectName, itemType, fields);
|
|
239
|
+
if (epicId) {
|
|
240
|
+
await this.linkWorkItems(workItem.id, epicId, "System.LinkTypes.Hierarchy-Reverse");
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
project: projectName,
|
|
244
|
+
workItemId: workItem.id,
|
|
245
|
+
workItemType: itemType,
|
|
246
|
+
title,
|
|
247
|
+
url: workItem.url,
|
|
248
|
+
action: "created",
|
|
249
|
+
confidence
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Create work item in area path (Pattern 2: Area Paths)
|
|
254
|
+
*/
|
|
255
|
+
async createWorkItemInAreaPath(areaPath, userStory, epicId) {
|
|
256
|
+
const title = `${userStory.id}: ${userStory.title}`;
|
|
257
|
+
const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
|
|
258
|
+
const description = `<h3>${userStory.title}</h3>
|
|
259
|
+
|
|
260
|
+
${userStory.description}
|
|
261
|
+
|
|
262
|
+
<h4>Acceptance Criteria</h4>
|
|
263
|
+
|
|
264
|
+
<ul>
|
|
265
|
+
${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join("\n")}
|
|
266
|
+
</ul>
|
|
267
|
+
|
|
268
|
+
${userStory.technicalContext ? `<h4>Technical Context</h4>
|
|
269
|
+
|
|
270
|
+
${userStory.technicalContext}
|
|
271
|
+
` : ""}
|
|
272
|
+
|
|
273
|
+
<p>\u{1F916} Auto-generated by SpecWeave</p>
|
|
274
|
+
`;
|
|
275
|
+
const fields = {
|
|
276
|
+
"System.Title": title,
|
|
277
|
+
"System.Description": description,
|
|
278
|
+
"System.AreaPath": `${this.config.project}\\${areaPath}`,
|
|
279
|
+
// Team-specific area path
|
|
280
|
+
"System.State": "New"
|
|
281
|
+
};
|
|
282
|
+
const workItem = await this.createWorkItem(this.config.project, itemType, fields);
|
|
283
|
+
if (epicId) {
|
|
284
|
+
await this.linkWorkItems(workItem.id, epicId, "System.LinkTypes.Hierarchy-Reverse");
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
project: this.config.project,
|
|
288
|
+
workItemId: workItem.id,
|
|
289
|
+
workItemType: itemType,
|
|
290
|
+
title,
|
|
291
|
+
url: workItem.url,
|
|
292
|
+
action: "created"
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Create work item via ADO REST API
|
|
297
|
+
*/
|
|
298
|
+
async createWorkItem(project, workItemType, fields) {
|
|
299
|
+
const patchDocument = Object.entries(fields).map(([key, value]) => ({
|
|
300
|
+
op: "add",
|
|
301
|
+
path: `/fields/${key}`,
|
|
302
|
+
value
|
|
303
|
+
}));
|
|
304
|
+
const response = await this.client.post(
|
|
305
|
+
`/${project}/_apis/wit/workitems/$${workItemType}?api-version=7.0`,
|
|
306
|
+
patchDocument
|
|
307
|
+
);
|
|
308
|
+
return response.data;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Link work items (parent-child relationship)
|
|
312
|
+
*/
|
|
313
|
+
async linkWorkItems(sourceId, targetId, linkType) {
|
|
314
|
+
const patchDocument = [
|
|
315
|
+
{
|
|
316
|
+
op: "add",
|
|
317
|
+
path: "/relations/-",
|
|
318
|
+
value: {
|
|
319
|
+
rel: linkType,
|
|
320
|
+
url: `https://dev.azure.com/${this.config.organization}/_apis/wit/workItems/${targetId}`
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
await this.client.patch(
|
|
325
|
+
`/_apis/wit/workitems/${sourceId}?api-version=7.0`,
|
|
326
|
+
patchDocument
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Map Jira-style item type to ADO work item type
|
|
331
|
+
*/
|
|
332
|
+
mapItemTypeToAdo(itemType) {
|
|
333
|
+
const mapping = this.config.workItemTypes || {};
|
|
334
|
+
switch (itemType) {
|
|
335
|
+
case "Epic":
|
|
336
|
+
return mapping.epic || "Epic";
|
|
337
|
+
case "Story":
|
|
338
|
+
return mapping.story || "User Story";
|
|
339
|
+
case "Task":
|
|
340
|
+
return mapping.task || "Task";
|
|
341
|
+
case "Subtask":
|
|
342
|
+
return mapping.task || "Task";
|
|
343
|
+
// ADO doesn't have subtasks, use Task
|
|
344
|
+
default:
|
|
345
|
+
return "User Story";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Find ADO project name for project ID
|
|
350
|
+
*
|
|
351
|
+
* Maps project IDs to ADO project names:
|
|
352
|
+
* - FE → FE-Project
|
|
353
|
+
* - BE → BE-Project
|
|
354
|
+
* - MOBILE → MOBILE-Project
|
|
355
|
+
*/
|
|
356
|
+
findProjectForId(projectId) {
|
|
357
|
+
if (!this.config.projects) return void 0;
|
|
358
|
+
let match = this.config.projects.find((project) => project.toLowerCase().includes(projectId.toLowerCase()));
|
|
359
|
+
if (!match) {
|
|
360
|
+
const fuzzyMap = {
|
|
361
|
+
FE: ["frontend", "web", "ui", "client", "fe"],
|
|
362
|
+
BE: ["backend", "api", "server", "be"],
|
|
363
|
+
MOBILE: ["mobile", "app", "ios", "android"],
|
|
364
|
+
INFRA: ["infra", "infrastructure", "devops", "platform"]
|
|
365
|
+
};
|
|
366
|
+
const keywords = fuzzyMap[projectId] || [];
|
|
367
|
+
match = this.config.projects.find(
|
|
368
|
+
(project) => keywords.some((keyword) => project.toLowerCase().includes(keyword))
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return match;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Find area path for project ID
|
|
375
|
+
*
|
|
376
|
+
* Maps project IDs to area paths:
|
|
377
|
+
* - FE → FE
|
|
378
|
+
* - BE → BE
|
|
379
|
+
* - MOBILE → MOBILE
|
|
380
|
+
*/
|
|
381
|
+
findAreaPathForProjectId(projectId) {
|
|
382
|
+
if (!this.config.areaPaths) return void 0;
|
|
383
|
+
let match = this.config.areaPaths.find((areaPath) => areaPath.toLowerCase() === projectId.toLowerCase());
|
|
384
|
+
if (!match) {
|
|
385
|
+
const fuzzyMap = {
|
|
386
|
+
FE: ["frontend", "web", "ui", "client", "fe"],
|
|
387
|
+
BE: ["backend", "api", "server", "be"],
|
|
388
|
+
MOBILE: ["mobile", "app", "ios", "android"],
|
|
389
|
+
INFRA: ["infra", "infrastructure", "devops", "platform"]
|
|
390
|
+
};
|
|
391
|
+
const keywords = fuzzyMap[projectId] || [];
|
|
392
|
+
match = this.config.areaPaths.find(
|
|
393
|
+
(areaPath) => keywords.some((keyword) => areaPath.toLowerCase().includes(keyword))
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return match;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function formatAdoSyncResults(results) {
|
|
400
|
+
const lines = [];
|
|
401
|
+
lines.push("\u{1F4CA} Azure DevOps Multi-Project Sync Results:\n");
|
|
402
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const result of results) {
|
|
404
|
+
const existing = byProject.get(result.project) || [];
|
|
405
|
+
existing.push(result);
|
|
406
|
+
byProject.set(result.project, existing);
|
|
407
|
+
}
|
|
408
|
+
for (const [project, projectResults] of byProject.entries()) {
|
|
409
|
+
lines.push(`
|
|
410
|
+
**ADO Project ${project}**:`);
|
|
411
|
+
for (const result of projectResults) {
|
|
412
|
+
const icon = result.action === "created" ? "\u2705" : result.action === "updated" ? "\u{1F504}" : "\u23ED\uFE0F";
|
|
413
|
+
const confidence = result.confidence !== void 0 ? ` (${(result.confidence * 100).toFixed(0)}% confidence)` : "";
|
|
414
|
+
lines.push(` ${icon} #${result.workItemId} [${result.workItemType}]: ${result.title}${confidence}`);
|
|
415
|
+
lines.push(` ${result.url}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
lines.push(`
|
|
419
|
+
\u2705 Total: ${results.length} work items synced
|
|
420
|
+
`);
|
|
421
|
+
const epicCount = results.filter((r) => r.workItemType === "Epic").length;
|
|
422
|
+
const featureCount = results.filter((r) => r.workItemType === "Feature").length;
|
|
423
|
+
const storyCount = results.filter((r) => r.workItemType === "User Story").length;
|
|
424
|
+
const taskCount = results.filter((r) => r.workItemType === "Task").length;
|
|
425
|
+
lines.push("\u{1F4C8} Work Item Type Distribution:");
|
|
426
|
+
if (epicCount > 0) lines.push(` - Epics: ${epicCount}`);
|
|
427
|
+
if (featureCount > 0) lines.push(` - Features: ${featureCount}`);
|
|
428
|
+
if (storyCount > 0) lines.push(` - User Stories: ${storyCount}`);
|
|
429
|
+
if (taskCount > 0) lines.push(` - Tasks: ${taskCount}`);
|
|
430
|
+
return lines.join("\n");
|
|
431
|
+
}
|
|
432
|
+
async function validateAdoProjects(config, projectNames) {
|
|
433
|
+
const missing = [];
|
|
434
|
+
const client = axios.create({
|
|
435
|
+
baseURL: `https://dev.azure.com/${config.organization}`,
|
|
436
|
+
headers: {
|
|
437
|
+
"Authorization": `Basic ${Buffer.from(":" + config.pat).toString("base64")}`
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
for (const name of projectNames) {
|
|
441
|
+
try {
|
|
442
|
+
await client.get(`/_apis/projects/${name}?api-version=7.0`);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
missing.push(name);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return missing;
|
|
448
|
+
}
|
|
449
|
+
export {
|
|
450
|
+
AdoMultiProjectSync,
|
|
451
|
+
formatAdoSyncResults,
|
|
452
|
+
validateAdoProjects
|
|
453
|
+
};
|