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,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure DevOps Multi-Project Sync
|
|
3
|
+
*
|
|
4
|
+
* Supports two patterns:
|
|
5
|
+
* 1. **Multiple Projects**: Separate ADO projects for each team (FE, BE, MOBILE)
|
|
6
|
+
* 2. **Single Project + Area Paths**: One ADO project with area paths per team
|
|
7
|
+
*
|
|
8
|
+
* Hierarchical work item types:
|
|
9
|
+
* - Epic (> 13 story points): Large feature area
|
|
10
|
+
* - Feature (8-13 story points): Medium feature
|
|
11
|
+
* - User Story (3-7 story points): Standard user story
|
|
12
|
+
* - Task (1-2 story points): Small implementation task
|
|
13
|
+
*
|
|
14
|
+
* @module ado-multi-project-sync
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import axios, { AxiosInstance } from 'axios';
|
|
18
|
+
import {
|
|
19
|
+
UserStory,
|
|
20
|
+
getPrimaryProject,
|
|
21
|
+
suggestJiraItemType,
|
|
22
|
+
mapUserStoryToProjects
|
|
23
|
+
} from '../../../src/utils/project-mapper.js';
|
|
24
|
+
import { parseSpecFile } from '../../../src/utils/spec-splitter.js';
|
|
25
|
+
|
|
26
|
+
export interface AdoMultiProjectConfig {
|
|
27
|
+
organization: string;
|
|
28
|
+
pat: string; // Personal Access Token
|
|
29
|
+
|
|
30
|
+
// Pattern 1: Multiple projects (simple)
|
|
31
|
+
projects?: string[]; // ['FE-Project', 'BE-Project', 'MOBILE-Project']
|
|
32
|
+
|
|
33
|
+
// Pattern 2: Single project + area paths (advanced)
|
|
34
|
+
project?: string; // 'Shared-Project'
|
|
35
|
+
areaPaths?: string[]; // ['FE', 'BE', 'MOBILE']
|
|
36
|
+
|
|
37
|
+
// Work item type mapping (optional)
|
|
38
|
+
workItemTypes?: {
|
|
39
|
+
epic: string; // Default: 'Epic'
|
|
40
|
+
feature: string; // Default: 'Feature'
|
|
41
|
+
story: string; // Default: 'User Story'
|
|
42
|
+
task: string; // Default: 'Task'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Settings
|
|
46
|
+
intelligentMapping?: boolean; // Default: true (auto-classify user stories)
|
|
47
|
+
autoCreateEpics?: boolean; // Default: true (create epic per project)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AdoWorkItem {
|
|
51
|
+
id: number;
|
|
52
|
+
rev: number;
|
|
53
|
+
fields: Record<string, any>;
|
|
54
|
+
url: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AdoSyncResult {
|
|
58
|
+
project: string;
|
|
59
|
+
workItemId: number;
|
|
60
|
+
workItemType: string;
|
|
61
|
+
title: string;
|
|
62
|
+
url: string;
|
|
63
|
+
action: 'created' | 'updated' | 'skipped';
|
|
64
|
+
confidence?: number; // Classification confidence (0.0-1.0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Azure DevOps Multi-Project Sync Client
|
|
69
|
+
*/
|
|
70
|
+
export class AdoMultiProjectSync {
|
|
71
|
+
private client: AxiosInstance;
|
|
72
|
+
private config: AdoMultiProjectConfig;
|
|
73
|
+
|
|
74
|
+
constructor(config: AdoMultiProjectConfig) {
|
|
75
|
+
this.config = config;
|
|
76
|
+
|
|
77
|
+
// Create axios instance with ADO authentication
|
|
78
|
+
this.client = axios.create({
|
|
79
|
+
baseURL: `https://dev.azure.com/${config.organization}`,
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json-patch+json',
|
|
82
|
+
'Authorization': `Basic ${Buffer.from(':' + config.pat).toString('base64')}`
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sync spec to ADO projects with intelligent mapping
|
|
89
|
+
*
|
|
90
|
+
* @param specPath Path to spec file
|
|
91
|
+
* @returns Array of sync results
|
|
92
|
+
*/
|
|
93
|
+
async syncSpec(specPath: string): Promise<AdoSyncResult[]> {
|
|
94
|
+
const results: AdoSyncResult[] = [];
|
|
95
|
+
|
|
96
|
+
// Parse spec
|
|
97
|
+
const parsedSpec = await parseSpecFile(specPath);
|
|
98
|
+
|
|
99
|
+
// Determine sync pattern
|
|
100
|
+
const isAreaPathBased = !!this.config.project && !!this.config.areaPaths;
|
|
101
|
+
|
|
102
|
+
if (isAreaPathBased) {
|
|
103
|
+
// Pattern 2: Single project + area paths
|
|
104
|
+
results.push(...await this.syncAreaPathBased(parsedSpec));
|
|
105
|
+
} else if (this.config.projects) {
|
|
106
|
+
// Pattern 1: Multiple projects
|
|
107
|
+
results.push(...await this.syncMultipleProjects(parsedSpec));
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error('Invalid config: Must specify projects[] or project+areaPaths[]');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Pattern 1: Sync to multiple ADO projects (simple)
|
|
117
|
+
*
|
|
118
|
+
* Each team → separate ADO project
|
|
119
|
+
* - FE user stories → FE-Project
|
|
120
|
+
* - BE user stories → BE-Project
|
|
121
|
+
* - MOBILE user stories → MOBILE-Project
|
|
122
|
+
*/
|
|
123
|
+
private async syncMultipleProjects(parsedSpec: any): Promise<AdoSyncResult[]> {
|
|
124
|
+
const results: AdoSyncResult[] = [];
|
|
125
|
+
|
|
126
|
+
// Step 1: Create epic per project (if enabled)
|
|
127
|
+
const epicsByProject = new Map<string, number>(); // projectName → epicId
|
|
128
|
+
|
|
129
|
+
if (this.config.autoCreateEpics !== false) {
|
|
130
|
+
for (const projectName of this.config.projects!) {
|
|
131
|
+
const epicResult = await this.createEpicForProject(parsedSpec, projectName);
|
|
132
|
+
epicsByProject.set(projectName, epicResult.workItemId);
|
|
133
|
+
results.push(epicResult);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Step 2: Classify user stories by project
|
|
138
|
+
const projectStories = new Map<string, Array<{ story: UserStory; confidence: number }>>();
|
|
139
|
+
|
|
140
|
+
for (const userStory of parsedSpec.userStories) {
|
|
141
|
+
if (this.config.intelligentMapping !== false) {
|
|
142
|
+
// Intelligent mapping (default)
|
|
143
|
+
const mappings = mapUserStoryToProjects(userStory);
|
|
144
|
+
|
|
145
|
+
if (mappings.length > 0 && mappings[0].confidence >= 0.3) {
|
|
146
|
+
const primary = mappings[0];
|
|
147
|
+
const projectName = this.findProjectForId(primary.projectId);
|
|
148
|
+
|
|
149
|
+
if (projectName) {
|
|
150
|
+
const existing = projectStories.get(projectName) || [];
|
|
151
|
+
existing.push({ story: userStory, confidence: primary.confidence });
|
|
152
|
+
projectStories.set(projectName, existing);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// No confident match - assign to first project or skip
|
|
156
|
+
console.warn(`⚠️ Low confidence for ${userStory.id} (${(mappings[0]?.confidence || 0) * 100}%) - assigning to ${this.config.projects![0]}`);
|
|
157
|
+
const fallback = this.config.projects![0];
|
|
158
|
+
const existing = projectStories.get(fallback) || [];
|
|
159
|
+
existing.push({ story: userStory, confidence: mappings[0]?.confidence || 0 });
|
|
160
|
+
projectStories.set(fallback, existing);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Step 3: Create work items in each project
|
|
166
|
+
for (const [projectName, stories] of projectStories.entries()) {
|
|
167
|
+
const epicId = epicsByProject.get(projectName);
|
|
168
|
+
|
|
169
|
+
for (const { story, confidence } of stories) {
|
|
170
|
+
const result = await this.createWorkItemForUserStory(projectName, story, epicId, confidence);
|
|
171
|
+
results.push(result);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Pattern 2: Sync to single project with area paths (advanced)
|
|
180
|
+
*
|
|
181
|
+
* - Single ADO project with area paths for teams
|
|
182
|
+
* - Epic-level: Root area path
|
|
183
|
+
* - Story-level: Team-specific area paths
|
|
184
|
+
*
|
|
185
|
+
* Example:
|
|
186
|
+
* ADO Project: Shared-Project
|
|
187
|
+
* Epic: User Authentication (Root area path)
|
|
188
|
+
* User Story: Login UI (Area Path: Shared-Project\FE)
|
|
189
|
+
* User Story: Auth API (Area Path: Shared-Project\BE)
|
|
190
|
+
* User Story: Mobile Auth (Area Path: Shared-Project\MOBILE)
|
|
191
|
+
*/
|
|
192
|
+
private async syncAreaPathBased(parsedSpec: any): Promise<AdoSyncResult[]> {
|
|
193
|
+
const results: AdoSyncResult[] = [];
|
|
194
|
+
|
|
195
|
+
if (!this.config.project || !this.config.areaPaths) {
|
|
196
|
+
throw new Error('Area path mode requires project and areaPaths');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Step 1: Create epic in root area path
|
|
200
|
+
const epicResult = await this.createEpicInRootArea(parsedSpec);
|
|
201
|
+
results.push(epicResult);
|
|
202
|
+
|
|
203
|
+
// Step 2: Classify user stories by area path
|
|
204
|
+
const areaPathStories = new Map<string, UserStory[]>();
|
|
205
|
+
|
|
206
|
+
for (const userStory of parsedSpec.userStories) {
|
|
207
|
+
const primaryProject = getPrimaryProject(userStory);
|
|
208
|
+
|
|
209
|
+
if (primaryProject) {
|
|
210
|
+
const areaPath = this.findAreaPathForProjectId(primaryProject.projectId);
|
|
211
|
+
|
|
212
|
+
if (areaPath) {
|
|
213
|
+
const existing = areaPathStories.get(areaPath) || [];
|
|
214
|
+
existing.push(userStory);
|
|
215
|
+
areaPathStories.set(areaPath, existing);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Step 3: Create work items in respective area paths
|
|
221
|
+
for (const [areaPath, stories] of areaPathStories.entries()) {
|
|
222
|
+
for (const story of stories) {
|
|
223
|
+
const result = await this.createWorkItemInAreaPath(areaPath, story, epicResult.workItemId);
|
|
224
|
+
results.push(result);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create epic for project (Pattern 1: Multiple Projects)
|
|
233
|
+
*/
|
|
234
|
+
private async createEpicForProject(parsedSpec: any, projectName: string): Promise<AdoSyncResult> {
|
|
235
|
+
const title = `${parsedSpec.metadata.title} - ${projectName}`;
|
|
236
|
+
|
|
237
|
+
const description = `<h2>${projectName} Implementation</h2>
|
|
238
|
+
|
|
239
|
+
<strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
|
|
240
|
+
<strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
|
|
241
|
+
<strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
|
|
242
|
+
|
|
243
|
+
<h3>Executive Summary</h3>
|
|
244
|
+
|
|
245
|
+
${parsedSpec.executiveSummary}
|
|
246
|
+
|
|
247
|
+
<h3>Scope (${projectName})</h3>
|
|
248
|
+
|
|
249
|
+
This epic covers all ${projectName}-related user stories for "${parsedSpec.metadata.title}".
|
|
250
|
+
|
|
251
|
+
User stories will be added as child work items.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
🤖 Auto-generated by SpecWeave
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
const workItem = await this.createWorkItem(projectName, this.config.workItemTypes?.epic || 'Epic', {
|
|
259
|
+
'System.Title': title,
|
|
260
|
+
'System.Description': description,
|
|
261
|
+
'System.State': 'New'
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
project: projectName,
|
|
266
|
+
workItemId: workItem.id,
|
|
267
|
+
workItemType: 'Epic',
|
|
268
|
+
title,
|
|
269
|
+
url: workItem.url,
|
|
270
|
+
action: 'created'
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Create epic in root area path (Pattern 2: Area Paths)
|
|
276
|
+
*/
|
|
277
|
+
private async createEpicInRootArea(parsedSpec: any): Promise<AdoSyncResult> {
|
|
278
|
+
const title = parsedSpec.metadata.title;
|
|
279
|
+
|
|
280
|
+
const description = `<h2>${parsedSpec.metadata.title}</h2>
|
|
281
|
+
|
|
282
|
+
<strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
|
|
283
|
+
<strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
|
|
284
|
+
<strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
|
|
285
|
+
|
|
286
|
+
<h3>Executive Summary</h3>
|
|
287
|
+
|
|
288
|
+
${parsedSpec.executiveSummary}
|
|
289
|
+
|
|
290
|
+
<h3>User Stories (${parsedSpec.userStories.length} total)</h3>
|
|
291
|
+
|
|
292
|
+
<ul>
|
|
293
|
+
${parsedSpec.userStories.map((s: UserStory, i: number) => `<li>${i + 1}. ${s.id}: ${s.title}</li>`).join('\n')}
|
|
294
|
+
</ul>
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
🤖 Auto-generated by SpecWeave
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
const workItem = await this.createWorkItem(this.config.project!, this.config.workItemTypes?.epic || 'Epic', {
|
|
302
|
+
'System.Title': title,
|
|
303
|
+
'System.Description': description,
|
|
304
|
+
'System.AreaPath': this.config.project!, // Root area path
|
|
305
|
+
'System.State': 'New'
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
project: this.config.project!,
|
|
310
|
+
workItemId: workItem.id,
|
|
311
|
+
workItemType: 'Epic',
|
|
312
|
+
title,
|
|
313
|
+
url: workItem.url,
|
|
314
|
+
action: 'created'
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create work item for user story (Pattern 1: Multiple Projects)
|
|
320
|
+
*/
|
|
321
|
+
private async createWorkItemForUserStory(
|
|
322
|
+
projectName: string,
|
|
323
|
+
userStory: UserStory,
|
|
324
|
+
epicId?: number,
|
|
325
|
+
confidence?: number
|
|
326
|
+
): Promise<AdoSyncResult> {
|
|
327
|
+
const title = `${userStory.id}: ${userStory.title}`;
|
|
328
|
+
|
|
329
|
+
// Determine work item type based on story points
|
|
330
|
+
const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
|
|
331
|
+
|
|
332
|
+
const description = `<h3>${userStory.title}</h3>
|
|
333
|
+
|
|
334
|
+
${userStory.description}
|
|
335
|
+
|
|
336
|
+
<h4>Acceptance Criteria</h4>
|
|
337
|
+
|
|
338
|
+
<ul>
|
|
339
|
+
${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join('\n')}
|
|
340
|
+
</ul>
|
|
341
|
+
|
|
342
|
+
${userStory.technicalContext ? `<h4>Technical Context</h4>\n\n${userStory.technicalContext}\n` : ''}
|
|
343
|
+
|
|
344
|
+
${confidence !== undefined ? `<p><em>Classification confidence: ${(confidence * 100).toFixed(0)}%</em></p>\n` : ''}
|
|
345
|
+
|
|
346
|
+
<p>🤖 Auto-generated by SpecWeave</p>
|
|
347
|
+
`;
|
|
348
|
+
|
|
349
|
+
const fields: Record<string, any> = {
|
|
350
|
+
'System.Title': title,
|
|
351
|
+
'System.Description': description,
|
|
352
|
+
'System.State': 'New'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const workItem = await this.createWorkItem(projectName, itemType, fields);
|
|
356
|
+
|
|
357
|
+
// Link to epic if provided
|
|
358
|
+
if (epicId) {
|
|
359
|
+
await this.linkWorkItems(workItem.id, epicId, 'System.LinkTypes.Hierarchy-Reverse');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
project: projectName,
|
|
364
|
+
workItemId: workItem.id,
|
|
365
|
+
workItemType: itemType,
|
|
366
|
+
title,
|
|
367
|
+
url: workItem.url,
|
|
368
|
+
action: 'created',
|
|
369
|
+
confidence
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Create work item in area path (Pattern 2: Area Paths)
|
|
375
|
+
*/
|
|
376
|
+
private async createWorkItemInAreaPath(
|
|
377
|
+
areaPath: string,
|
|
378
|
+
userStory: UserStory,
|
|
379
|
+
epicId?: number
|
|
380
|
+
): Promise<AdoSyncResult> {
|
|
381
|
+
const title = `${userStory.id}: ${userStory.title}`;
|
|
382
|
+
|
|
383
|
+
// Determine work item type based on story points
|
|
384
|
+
const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
|
|
385
|
+
|
|
386
|
+
const description = `<h3>${userStory.title}</h3>
|
|
387
|
+
|
|
388
|
+
${userStory.description}
|
|
389
|
+
|
|
390
|
+
<h4>Acceptance Criteria</h4>
|
|
391
|
+
|
|
392
|
+
<ul>
|
|
393
|
+
${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join('\n')}
|
|
394
|
+
</ul>
|
|
395
|
+
|
|
396
|
+
${userStory.technicalContext ? `<h4>Technical Context</h4>\n\n${userStory.technicalContext}\n` : ''}
|
|
397
|
+
|
|
398
|
+
<p>🤖 Auto-generated by SpecWeave</p>
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
const fields: Record<string, any> = {
|
|
402
|
+
'System.Title': title,
|
|
403
|
+
'System.Description': description,
|
|
404
|
+
'System.AreaPath': `${this.config.project}\\${areaPath}`, // Team-specific area path
|
|
405
|
+
'System.State': 'New'
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const workItem = await this.createWorkItem(this.config.project!, itemType, fields);
|
|
409
|
+
|
|
410
|
+
// Link to epic if provided
|
|
411
|
+
if (epicId) {
|
|
412
|
+
await this.linkWorkItems(workItem.id, epicId, 'System.LinkTypes.Hierarchy-Reverse');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
project: this.config.project!,
|
|
417
|
+
workItemId: workItem.id,
|
|
418
|
+
workItemType: itemType,
|
|
419
|
+
title,
|
|
420
|
+
url: workItem.url,
|
|
421
|
+
action: 'created'
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create work item via ADO REST API
|
|
427
|
+
*/
|
|
428
|
+
private async createWorkItem(
|
|
429
|
+
project: string,
|
|
430
|
+
workItemType: string,
|
|
431
|
+
fields: Record<string, any>
|
|
432
|
+
): Promise<AdoWorkItem> {
|
|
433
|
+
// Build JSON patch document
|
|
434
|
+
const patchDocument = Object.entries(fields).map(([key, value]) => ({
|
|
435
|
+
op: 'add',
|
|
436
|
+
path: `/fields/${key}`,
|
|
437
|
+
value
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
const response = await this.client.post(
|
|
441
|
+
`/${project}/_apis/wit/workitems/$${workItemType}?api-version=7.0`,
|
|
442
|
+
patchDocument
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
return response.data;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Link work items (parent-child relationship)
|
|
450
|
+
*/
|
|
451
|
+
private async linkWorkItems(
|
|
452
|
+
sourceId: number,
|
|
453
|
+
targetId: number,
|
|
454
|
+
linkType: string
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
const patchDocument = [
|
|
457
|
+
{
|
|
458
|
+
op: 'add',
|
|
459
|
+
path: '/relations/-',
|
|
460
|
+
value: {
|
|
461
|
+
rel: linkType,
|
|
462
|
+
url: `https://dev.azure.com/${this.config.organization}/_apis/wit/workItems/${targetId}`
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
await this.client.patch(
|
|
468
|
+
`/_apis/wit/workitems/${sourceId}?api-version=7.0`,
|
|
469
|
+
patchDocument
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Map Jira-style item type to ADO work item type
|
|
475
|
+
*/
|
|
476
|
+
private mapItemTypeToAdo(itemType: 'Epic' | 'Story' | 'Task' | 'Subtask'): string {
|
|
477
|
+
const mapping = this.config.workItemTypes || {};
|
|
478
|
+
|
|
479
|
+
switch (itemType) {
|
|
480
|
+
case 'Epic':
|
|
481
|
+
return mapping.epic || 'Epic';
|
|
482
|
+
case 'Story':
|
|
483
|
+
return mapping.story || 'User Story';
|
|
484
|
+
case 'Task':
|
|
485
|
+
return mapping.task || 'Task';
|
|
486
|
+
case 'Subtask':
|
|
487
|
+
return mapping.task || 'Task'; // ADO doesn't have subtasks, use Task
|
|
488
|
+
default:
|
|
489
|
+
return 'User Story';
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Find ADO project name for project ID
|
|
495
|
+
*
|
|
496
|
+
* Maps project IDs to ADO project names:
|
|
497
|
+
* - FE → FE-Project
|
|
498
|
+
* - BE → BE-Project
|
|
499
|
+
* - MOBILE → MOBILE-Project
|
|
500
|
+
*/
|
|
501
|
+
private findProjectForId(projectId: string): string | undefined {
|
|
502
|
+
if (!this.config.projects) return undefined;
|
|
503
|
+
|
|
504
|
+
// Try exact match first
|
|
505
|
+
let match = this.config.projects.find(project => project.toLowerCase().includes(projectId.toLowerCase()));
|
|
506
|
+
|
|
507
|
+
if (!match) {
|
|
508
|
+
// Try fuzzy match (FE → frontend, BE → backend, MOBILE → mobile)
|
|
509
|
+
const fuzzyMap: Record<string, string[]> = {
|
|
510
|
+
FE: ['frontend', 'web', 'ui', 'client', 'fe'],
|
|
511
|
+
BE: ['backend', 'api', 'server', 'be'],
|
|
512
|
+
MOBILE: ['mobile', 'app', 'ios', 'android'],
|
|
513
|
+
INFRA: ['infra', 'infrastructure', 'devops', 'platform']
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const keywords = fuzzyMap[projectId] || [];
|
|
517
|
+
|
|
518
|
+
match = this.config.projects.find(project =>
|
|
519
|
+
keywords.some(keyword => project.toLowerCase().includes(keyword))
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return match;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Find area path for project ID
|
|
528
|
+
*
|
|
529
|
+
* Maps project IDs to area paths:
|
|
530
|
+
* - FE → FE
|
|
531
|
+
* - BE → BE
|
|
532
|
+
* - MOBILE → MOBILE
|
|
533
|
+
*/
|
|
534
|
+
private findAreaPathForProjectId(projectId: string): string | undefined {
|
|
535
|
+
if (!this.config.areaPaths) return undefined;
|
|
536
|
+
|
|
537
|
+
// Try exact match first
|
|
538
|
+
let match = this.config.areaPaths.find(areaPath => areaPath.toLowerCase() === projectId.toLowerCase());
|
|
539
|
+
|
|
540
|
+
if (!match) {
|
|
541
|
+
// Try fuzzy match
|
|
542
|
+
const fuzzyMap: Record<string, string[]> = {
|
|
543
|
+
FE: ['frontend', 'web', 'ui', 'client', 'fe'],
|
|
544
|
+
BE: ['backend', 'api', 'server', 'be'],
|
|
545
|
+
MOBILE: ['mobile', 'app', 'ios', 'android'],
|
|
546
|
+
INFRA: ['infra', 'infrastructure', 'devops', 'platform']
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const keywords = fuzzyMap[projectId] || [];
|
|
550
|
+
|
|
551
|
+
match = this.config.areaPaths.find(areaPath =>
|
|
552
|
+
keywords.some(keyword => areaPath.toLowerCase().includes(keyword))
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return match;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Format ADO sync results for display
|
|
562
|
+
*/
|
|
563
|
+
export function formatAdoSyncResults(results: AdoSyncResult[]): string {
|
|
564
|
+
const lines: string[] = [];
|
|
565
|
+
|
|
566
|
+
lines.push('📊 Azure DevOps Multi-Project Sync Results:\n');
|
|
567
|
+
|
|
568
|
+
const byProject = new Map<string, AdoSyncResult[]>();
|
|
569
|
+
|
|
570
|
+
for (const result of results) {
|
|
571
|
+
const existing = byProject.get(result.project) || [];
|
|
572
|
+
existing.push(result);
|
|
573
|
+
byProject.set(result.project, existing);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
for (const [project, projectResults] of byProject.entries()) {
|
|
577
|
+
lines.push(`\n**ADO Project ${project}**:`);
|
|
578
|
+
|
|
579
|
+
for (const result of projectResults) {
|
|
580
|
+
const icon = result.action === 'created' ? '✅' : result.action === 'updated' ? '🔄' : '⏭️';
|
|
581
|
+
const confidence = result.confidence !== undefined ? ` (${(result.confidence * 100).toFixed(0)}% confidence)` : '';
|
|
582
|
+
lines.push(` ${icon} #${result.workItemId} [${result.workItemType}]: ${result.title}${confidence}`);
|
|
583
|
+
lines.push(` ${result.url}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
lines.push(`\n✅ Total: ${results.length} work items synced\n`);
|
|
588
|
+
|
|
589
|
+
// Show work item type distribution
|
|
590
|
+
const epicCount = results.filter(r => r.workItemType === 'Epic').length;
|
|
591
|
+
const featureCount = results.filter(r => r.workItemType === 'Feature').length;
|
|
592
|
+
const storyCount = results.filter(r => r.workItemType === 'User Story').length;
|
|
593
|
+
const taskCount = results.filter(r => r.workItemType === 'Task').length;
|
|
594
|
+
|
|
595
|
+
lines.push('📈 Work Item Type Distribution:');
|
|
596
|
+
if (epicCount > 0) lines.push(` - Epics: ${epicCount}`);
|
|
597
|
+
if (featureCount > 0) lines.push(` - Features: ${featureCount}`);
|
|
598
|
+
if (storyCount > 0) lines.push(` - User Stories: ${storyCount}`);
|
|
599
|
+
if (taskCount > 0) lines.push(` - Tasks: ${taskCount}`);
|
|
600
|
+
|
|
601
|
+
return lines.join('\n');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Validate ADO projects exist
|
|
606
|
+
*
|
|
607
|
+
* @param config ADO configuration
|
|
608
|
+
* @param projectNames Array of project names to validate
|
|
609
|
+
* @returns Validation results (missing projects)
|
|
610
|
+
*/
|
|
611
|
+
export async function validateAdoProjects(
|
|
612
|
+
config: AdoMultiProjectConfig,
|
|
613
|
+
projectNames: string[]
|
|
614
|
+
): Promise<string[]> {
|
|
615
|
+
const missing: string[] = [];
|
|
616
|
+
|
|
617
|
+
const client = axios.create({
|
|
618
|
+
baseURL: `https://dev.azure.com/${config.organization}`,
|
|
619
|
+
headers: {
|
|
620
|
+
'Authorization': `Basic ${Buffer.from(':' + config.pat).toString('base64')}`
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
for (const name of projectNames) {
|
|
625
|
+
try {
|
|
626
|
+
await client.get(`/_apis/projects/${name}?api-version=7.0`);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
missing.push(name);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return missing;
|
|
633
|
+
}
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docusaurus
|
|
3
|
-
description: Expert in generating Docusaurus documentation sites from SpecWeave structure. Converts .specweave/docs/public/ into
|
|
3
|
+
description: Expert in generating beautiful Docusaurus documentation sites from SpecWeave structure. ALWAYS use this for internal docs preview, browsing docs locally, viewing documentation in browser. Converts .specweave/docs/internal/ and .specweave/docs/public/ into beautiful, searchable sites with navigation, search, and Mermaid diagrams. Activates for preview internal docs, view docs locally, see docs in browser, browse documentation, internal docs preview, docusaurus, create docs site, generate documentation, public docs, documentation site, host docs, deploy documentation, static site generator, client documentation, engineering playbook, dual docs sites.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Docusaurus Documentation Generator
|
|
7
7
|
|
|
8
|
-
Expert skill for creating production-ready Docusaurus documentation sites from SpecWeave
|
|
8
|
+
Expert skill for creating production-ready Docusaurus documentation sites from SpecWeave documentation.
|
|
9
|
+
|
|
10
|
+
## 🚨 CRITICAL: Default Behavior for Internal Docs Preview
|
|
11
|
+
|
|
12
|
+
**When users ask to preview/view/browse internal docs locally:**
|
|
13
|
+
|
|
14
|
+
1. **IMMEDIATELY** invoke `/specweave-docs-preview:preview` command
|
|
15
|
+
2. This launches a beautiful Docusaurus site with:
|
|
16
|
+
- Auto-generated navigation from folder structure
|
|
17
|
+
- Search functionality
|
|
18
|
+
- Mermaid diagram rendering
|
|
19
|
+
- Hot reload for live editing
|
|
20
|
+
- Professional theming
|
|
21
|
+
3. **DO NOT** use basic file serving or simple markdown rendering
|
|
22
|
+
4. **ALWAYS** prefer Docusaurus for the best user experience
|
|
9
23
|
|
|
10
24
|
## What This Skill Does
|
|
11
25
|
|
|
12
|
-
Converts your SpecWeave
|
|
26
|
+
Converts your SpecWeave documentation (`.specweave/docs/internal/` or `.specweave/docs/public/`) into a beautiful, searchable, deployable documentation website using Docusaurus v3.
|
|
13
27
|
|
|
14
28
|
## Key Capabilities
|
|
15
29
|
|