specweave 0.16.5 → 0.17.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/bin/fix-marketplace-errors.sh +136 -0
- package/dist/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/cli/helpers/issue-tracker/index.js +21 -0
- package/dist/cli/helpers/issue-tracker/index.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave-ado/agents/ado-multi-project-mapper/AGENT.md +521 -0
- package/plugins/specweave-ado/agents/ado-sync-judge/AGENT.md +418 -0
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh +353 -0
- package/plugins/specweave-ado/lib/ado-project-detector.js +469 -0
- package/plugins/specweave-ado/lib/ado-project-detector.ts +510 -0
- package/plugins/specweave-ado/lib/conflict-resolver.js +297 -0
- package/plugins/specweave-ado/lib/conflict-resolver.ts +443 -0
- package/plugins/specweave-ado/skills/ado-multi-project/SKILL.md +541 -0
- package/plugins/specweave-ado/skills/ado-resource-validator/SKILL.md +719 -0
- package/src/templates/CLAUDE.md.template +24 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure DevOps Project Detector
|
|
3
|
+
*
|
|
4
|
+
* Intelligently detects which Azure DevOps project a spec or increment belongs to
|
|
5
|
+
* based on content analysis, folder structure, and configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs-extra';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { AzureDevOpsStrategy } from '../../../src/cli/helpers/issue-tracker/types';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export interface ProjectConfidence {
|
|
17
|
+
project: string;
|
|
18
|
+
confidence: number;
|
|
19
|
+
reasons: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProjectDetectionResult {
|
|
23
|
+
primary: string;
|
|
24
|
+
secondary?: string[];
|
|
25
|
+
confidence: number;
|
|
26
|
+
strategy: AzureDevOpsStrategy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProjectKeywords {
|
|
30
|
+
[project: string]: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProjectPatterns {
|
|
34
|
+
[project: string]: RegExp[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Project Detection Keywords and Patterns
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Keywords that indicate a spec belongs to a specific project
|
|
43
|
+
*/
|
|
44
|
+
export const PROJECT_KEYWORDS: ProjectKeywords = {
|
|
45
|
+
'AuthService': [
|
|
46
|
+
'authentication', 'auth', 'login', 'logout', 'oauth',
|
|
47
|
+
'jwt', 'token', 'session', 'password', 'credential',
|
|
48
|
+
'sso', 'saml', 'ldap', 'mfa', '2fa', 'totp'
|
|
49
|
+
],
|
|
50
|
+
'UserService': [
|
|
51
|
+
'user', 'profile', 'account', 'registration', 'preferences',
|
|
52
|
+
'settings', 'avatar', 'username', 'email', 'verification',
|
|
53
|
+
'onboarding', 'demographics', 'personalization'
|
|
54
|
+
],
|
|
55
|
+
'PaymentService': [
|
|
56
|
+
'payment', 'billing', 'stripe', 'paypal', 'invoice',
|
|
57
|
+
'subscription', 'charge', 'refund', 'credit card', 'transaction',
|
|
58
|
+
'checkout', 'cart', 'pricing', 'plan', 'tier'
|
|
59
|
+
],
|
|
60
|
+
'NotificationService': [
|
|
61
|
+
'notification', 'email', 'sms', 'push', 'alert',
|
|
62
|
+
'message', 'webhook', 'queue', 'sendgrid', 'twilio',
|
|
63
|
+
'template', 'broadcast', 'digest', 'reminder'
|
|
64
|
+
],
|
|
65
|
+
'Platform': [
|
|
66
|
+
'infrastructure', 'deployment', 'monitoring', 'logging',
|
|
67
|
+
'metrics', 'kubernetes', 'docker', 'ci/cd', 'pipeline',
|
|
68
|
+
'terraform', 'ansible', 'helm', 'grafana', 'prometheus'
|
|
69
|
+
],
|
|
70
|
+
'DataService': [
|
|
71
|
+
'database', 'data', 'analytics', 'etl', 'warehouse',
|
|
72
|
+
'pipeline', 'kafka', 'spark', 'hadoop', 'bigquery',
|
|
73
|
+
'redshift', 'snowflake', 'datalake', 'streaming'
|
|
74
|
+
],
|
|
75
|
+
'ApiGateway': [
|
|
76
|
+
'gateway', 'api', 'proxy', 'routing', 'load balancer',
|
|
77
|
+
'rate limiting', 'throttling', 'circuit breaker', 'cors',
|
|
78
|
+
'authentication proxy', 'service mesh', 'envoy', 'kong'
|
|
79
|
+
],
|
|
80
|
+
'WebApp': [
|
|
81
|
+
'frontend', 'ui', 'react', 'angular', 'vue', 'component',
|
|
82
|
+
'responsive', 'mobile-first', 'spa', 'ssr', 'next.js',
|
|
83
|
+
'gatsby', 'webpack', 'css', 'sass', 'styled-components'
|
|
84
|
+
],
|
|
85
|
+
'MobileApp': [
|
|
86
|
+
'ios', 'android', 'react native', 'flutter', 'swift',
|
|
87
|
+
'kotlin', 'objective-c', 'java', 'push notification',
|
|
88
|
+
'app store', 'play store', 'mobile', 'tablet', 'responsive'
|
|
89
|
+
]
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* File path patterns that indicate project ownership
|
|
94
|
+
*/
|
|
95
|
+
export const FILE_PATTERNS: ProjectPatterns = {
|
|
96
|
+
'AuthService': [
|
|
97
|
+
/auth\//i,
|
|
98
|
+
/login\//i,
|
|
99
|
+
/security\//i,
|
|
100
|
+
/oauth\//i,
|
|
101
|
+
/jwt\//i
|
|
102
|
+
],
|
|
103
|
+
'UserService': [
|
|
104
|
+
/users?\//i,
|
|
105
|
+
/profiles?\//i,
|
|
106
|
+
/accounts?\//i,
|
|
107
|
+
/members?\//i
|
|
108
|
+
],
|
|
109
|
+
'PaymentService': [
|
|
110
|
+
/payment\//i,
|
|
111
|
+
/billing\//i,
|
|
112
|
+
/checkout\//i,
|
|
113
|
+
/stripe\//i,
|
|
114
|
+
/subscription\//i
|
|
115
|
+
],
|
|
116
|
+
'NotificationService': [
|
|
117
|
+
/notification\//i,
|
|
118
|
+
/email\//i,
|
|
119
|
+
/messaging\//i,
|
|
120
|
+
/templates?\//i
|
|
121
|
+
],
|
|
122
|
+
'Platform': [
|
|
123
|
+
/infrastructure\//i,
|
|
124
|
+
/terraform\//i,
|
|
125
|
+
/kubernetes\//i,
|
|
126
|
+
/k8s\//i,
|
|
127
|
+
/\.github\/workflows\//i
|
|
128
|
+
],
|
|
129
|
+
'WebApp': [
|
|
130
|
+
/frontend\//i,
|
|
131
|
+
/src\/components\//i,
|
|
132
|
+
/src\/pages\//i,
|
|
133
|
+
/public\//i,
|
|
134
|
+
/styles?\//i
|
|
135
|
+
],
|
|
136
|
+
'MobileApp': [
|
|
137
|
+
/ios\//i,
|
|
138
|
+
/android\//i,
|
|
139
|
+
/mobile\//i,
|
|
140
|
+
/app\//i
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Project Detector Class
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
export class AdoProjectDetector {
|
|
149
|
+
private strategy: AzureDevOpsStrategy;
|
|
150
|
+
private availableProjects: string[];
|
|
151
|
+
private projectKeywords: ProjectKeywords;
|
|
152
|
+
private filePatterns: ProjectPatterns;
|
|
153
|
+
|
|
154
|
+
constructor(
|
|
155
|
+
strategy: AzureDevOpsStrategy,
|
|
156
|
+
availableProjects: string[],
|
|
157
|
+
customKeywords?: ProjectKeywords,
|
|
158
|
+
customPatterns?: ProjectPatterns
|
|
159
|
+
) {
|
|
160
|
+
this.strategy = strategy;
|
|
161
|
+
this.availableProjects = availableProjects;
|
|
162
|
+
this.projectKeywords = { ...PROJECT_KEYWORDS, ...customKeywords };
|
|
163
|
+
this.filePatterns = { ...FILE_PATTERNS, ...customPatterns };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Detect project from spec file path
|
|
168
|
+
*/
|
|
169
|
+
async detectFromSpecPath(specPath: string): Promise<ProjectDetectionResult> {
|
|
170
|
+
// For project-per-team strategy, use folder structure
|
|
171
|
+
if (this.strategy === 'project-per-team') {
|
|
172
|
+
const pathParts = specPath.split(path.sep);
|
|
173
|
+
const specsIndex = pathParts.indexOf('specs');
|
|
174
|
+
|
|
175
|
+
if (specsIndex !== -1 && specsIndex < pathParts.length - 1) {
|
|
176
|
+
const projectFolder = pathParts[specsIndex + 1];
|
|
177
|
+
|
|
178
|
+
// Check if it matches an available project
|
|
179
|
+
const matchedProject = this.availableProjects.find(
|
|
180
|
+
p => p.toLowerCase() === projectFolder.toLowerCase()
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (matchedProject) {
|
|
184
|
+
return {
|
|
185
|
+
primary: matchedProject,
|
|
186
|
+
confidence: 1.0,
|
|
187
|
+
strategy: this.strategy
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fall back to content detection
|
|
194
|
+
const content = await fs.readFile(specPath, 'utf-8');
|
|
195
|
+
return this.detectFromContent(content);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect project from spec content
|
|
200
|
+
*/
|
|
201
|
+
async detectFromContent(content: string): Promise<ProjectDetectionResult> {
|
|
202
|
+
const candidates = this.analyzeContent(content);
|
|
203
|
+
|
|
204
|
+
// High confidence: Auto-select
|
|
205
|
+
if (candidates[0]?.confidence > 0.7) {
|
|
206
|
+
return {
|
|
207
|
+
primary: candidates[0].project,
|
|
208
|
+
confidence: candidates[0].confidence,
|
|
209
|
+
strategy: this.strategy
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Medium confidence: Primary with secondary projects
|
|
214
|
+
if (candidates[0]?.confidence > 0.4) {
|
|
215
|
+
const secondary = candidates
|
|
216
|
+
.slice(1)
|
|
217
|
+
.filter(c => c.confidence > 0.3)
|
|
218
|
+
.map(c => c.project);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
primary: candidates[0].project,
|
|
222
|
+
secondary: secondary.length > 0 ? secondary : undefined,
|
|
223
|
+
confidence: candidates[0].confidence,
|
|
224
|
+
strategy: this.strategy
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Low confidence: Default to first available project
|
|
229
|
+
return {
|
|
230
|
+
primary: this.availableProjects[0] || 'Unknown',
|
|
231
|
+
confidence: 0,
|
|
232
|
+
strategy: this.strategy
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Analyze content and return project candidates with confidence scores
|
|
238
|
+
*/
|
|
239
|
+
private analyzeContent(content: string): ProjectConfidence[] {
|
|
240
|
+
const results: ProjectConfidence[] = [];
|
|
241
|
+
const lowerContent = content.toLowerCase();
|
|
242
|
+
|
|
243
|
+
for (const project of this.availableProjects) {
|
|
244
|
+
let confidence = 0;
|
|
245
|
+
const reasons: string[] = [];
|
|
246
|
+
|
|
247
|
+
// Check if project name is in title or frontmatter
|
|
248
|
+
if (lowerContent.includes(project.toLowerCase())) {
|
|
249
|
+
confidence += 0.5;
|
|
250
|
+
reasons.push(`Project name "${project}" found in content`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check keywords
|
|
254
|
+
const keywords = this.projectKeywords[project] || [];
|
|
255
|
+
let keywordMatches = 0;
|
|
256
|
+
for (const keyword of keywords) {
|
|
257
|
+
if (lowerContent.includes(keyword.toLowerCase())) {
|
|
258
|
+
keywordMatches++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (keywordMatches > 0) {
|
|
262
|
+
const keywordScore = Math.min(keywordMatches * 0.1, 0.4);
|
|
263
|
+
confidence += keywordScore;
|
|
264
|
+
reasons.push(`Found ${keywordMatches} keyword matches`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check file references
|
|
268
|
+
const patterns = this.filePatterns[project] || [];
|
|
269
|
+
let patternMatches = 0;
|
|
270
|
+
for (const pattern of patterns) {
|
|
271
|
+
if (pattern.test(content)) {
|
|
272
|
+
patternMatches++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (patternMatches > 0) {
|
|
276
|
+
const patternScore = Math.min(patternMatches * 0.15, 0.3);
|
|
277
|
+
confidence += patternScore;
|
|
278
|
+
reasons.push(`Found ${patternMatches} file pattern matches`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check for explicit project assignment
|
|
282
|
+
const projectAssignment = new RegExp(`project:\\s*${project}`, 'i');
|
|
283
|
+
if (projectAssignment.test(content)) {
|
|
284
|
+
confidence = 1.0; // Override with full confidence
|
|
285
|
+
reasons.push('Explicit project assignment in frontmatter');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
results.push({ project, confidence, reasons });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Sort by confidence (highest first)
|
|
292
|
+
return results.sort((a, b) => b.confidence - a.confidence);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Detect projects for multi-project spec
|
|
297
|
+
*/
|
|
298
|
+
async detectMultiProject(content: string): Promise<ProjectDetectionResult> {
|
|
299
|
+
const candidates = this.analyzeContent(content);
|
|
300
|
+
|
|
301
|
+
// Get all projects with meaningful confidence
|
|
302
|
+
const significantProjects = candidates.filter(c => c.confidence > 0.3);
|
|
303
|
+
|
|
304
|
+
if (significantProjects.length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
primary: this.availableProjects[0] || 'Unknown',
|
|
307
|
+
confidence: 0,
|
|
308
|
+
strategy: this.strategy
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Primary is highest confidence
|
|
313
|
+
const primary = significantProjects[0];
|
|
314
|
+
|
|
315
|
+
// Secondary are other significant projects
|
|
316
|
+
const secondary = significantProjects
|
|
317
|
+
.slice(1)
|
|
318
|
+
.map(c => c.project);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
primary: primary.project,
|
|
322
|
+
secondary: secondary.length > 0 ? secondary : undefined,
|
|
323
|
+
confidence: primary.confidence,
|
|
324
|
+
strategy: this.strategy
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Map spec to area path (for area-path-based strategy)
|
|
330
|
+
*/
|
|
331
|
+
mapToAreaPath(content: string, project: string): string {
|
|
332
|
+
const areaPaths = process.env.AZURE_DEVOPS_AREA_PATHS?.split(',').map(a => a.trim()) || [];
|
|
333
|
+
|
|
334
|
+
for (const areaPath of areaPaths) {
|
|
335
|
+
if (content.toLowerCase().includes(areaPath.toLowerCase())) {
|
|
336
|
+
return `${project}\\${areaPath}`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Default area path
|
|
341
|
+
return project;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Assign to team (for team-based strategy)
|
|
346
|
+
*/
|
|
347
|
+
assignToTeam(content: string): string {
|
|
348
|
+
const teams = process.env.AZURE_DEVOPS_TEAMS?.split(',').map(t => t.trim()) || [];
|
|
349
|
+
|
|
350
|
+
// Check for explicit team assignment
|
|
351
|
+
const teamMatch = content.match(/team:\s*([^\n]+)/i);
|
|
352
|
+
if (teamMatch) {
|
|
353
|
+
const assignedTeam = teamMatch[1].trim();
|
|
354
|
+
if (teams.includes(assignedTeam)) {
|
|
355
|
+
return assignedTeam;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Auto-detect based on keywords
|
|
360
|
+
const teamKeywords: { [team: string]: string[] } = {
|
|
361
|
+
'Frontend': ['ui', 'react', 'component', 'css', 'design'],
|
|
362
|
+
'Backend': ['api', 'database', 'server', 'endpoint', 'query'],
|
|
363
|
+
'Mobile': ['ios', 'android', 'app', 'native', 'push'],
|
|
364
|
+
'DevOps': ['deploy', 'ci/cd', 'kubernetes', 'docker', 'pipeline'],
|
|
365
|
+
'Data': ['analytics', 'etl', 'warehouse', 'bigquery', 'spark']
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
for (const team of teams) {
|
|
369
|
+
const keywords = teamKeywords[team] || [];
|
|
370
|
+
for (const keyword of keywords) {
|
|
371
|
+
if (content.toLowerCase().includes(keyword)) {
|
|
372
|
+
return team;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Default to first team
|
|
378
|
+
return teams[0] || 'Default Team';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// Utility Functions
|
|
384
|
+
// ============================================================================
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get project detector from environment
|
|
388
|
+
*/
|
|
389
|
+
export function getProjectDetectorFromEnv(): AdoProjectDetector {
|
|
390
|
+
const strategy = process.env.AZURE_DEVOPS_STRATEGY as AzureDevOpsStrategy || 'team-based';
|
|
391
|
+
|
|
392
|
+
let projects: string[] = [];
|
|
393
|
+
|
|
394
|
+
switch (strategy) {
|
|
395
|
+
case 'project-per-team':
|
|
396
|
+
projects = process.env.AZURE_DEVOPS_PROJECTS?.split(',').map(p => p.trim()) || [];
|
|
397
|
+
break;
|
|
398
|
+
case 'area-path-based':
|
|
399
|
+
case 'team-based':
|
|
400
|
+
const project = process.env.AZURE_DEVOPS_PROJECT;
|
|
401
|
+
if (project) {
|
|
402
|
+
projects = [project];
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return new AdoProjectDetector(strategy, projects);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Create project folders based on strategy
|
|
412
|
+
*/
|
|
413
|
+
export async function createProjectFolders(
|
|
414
|
+
baseDir: string,
|
|
415
|
+
strategy: AzureDevOpsStrategy,
|
|
416
|
+
projects: string[]
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
const specsPath = path.join(baseDir, '.specweave', 'docs', 'internal', 'specs');
|
|
419
|
+
|
|
420
|
+
switch (strategy) {
|
|
421
|
+
case 'project-per-team':
|
|
422
|
+
// Create folder for each project
|
|
423
|
+
for (const project of projects) {
|
|
424
|
+
const projectPath = path.join(specsPath, project);
|
|
425
|
+
await fs.ensureDir(projectPath);
|
|
426
|
+
await createProjectReadme(projectPath, project);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
|
|
430
|
+
case 'area-path-based':
|
|
431
|
+
// Create folders for area paths
|
|
432
|
+
const areaPaths = process.env.AZURE_DEVOPS_AREA_PATHS?.split(',').map(a => a.trim()) || [];
|
|
433
|
+
const project = projects[0];
|
|
434
|
+
if (project) {
|
|
435
|
+
const projectPath = path.join(specsPath, project);
|
|
436
|
+
await fs.ensureDir(projectPath);
|
|
437
|
+
|
|
438
|
+
for (const area of areaPaths) {
|
|
439
|
+
const areaPath = path.join(projectPath, area);
|
|
440
|
+
await fs.ensureDir(areaPath);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case 'team-based':
|
|
446
|
+
// Create folders for teams
|
|
447
|
+
const teams = process.env.AZURE_DEVOPS_TEAMS?.split(',').map(t => t.trim()) || [];
|
|
448
|
+
const proj = projects[0];
|
|
449
|
+
if (proj) {
|
|
450
|
+
const projectPath = path.join(specsPath, proj);
|
|
451
|
+
await fs.ensureDir(projectPath);
|
|
452
|
+
|
|
453
|
+
for (const team of teams) {
|
|
454
|
+
const teamPath = path.join(projectPath, team);
|
|
455
|
+
await fs.ensureDir(teamPath);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create README for project folder
|
|
464
|
+
*/
|
|
465
|
+
async function createProjectReadme(projectPath: string, projectName: string): Promise<void> {
|
|
466
|
+
const readmePath = path.join(projectPath, 'README.md');
|
|
467
|
+
|
|
468
|
+
// Don't overwrite existing README
|
|
469
|
+
if (await fs.pathExists(readmePath)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const content = `# ${projectName} Specifications
|
|
474
|
+
|
|
475
|
+
## Overview
|
|
476
|
+
|
|
477
|
+
This folder contains specifications for the ${projectName} project.
|
|
478
|
+
|
|
479
|
+
## Azure DevOps
|
|
480
|
+
|
|
481
|
+
- Organization: ${process.env.AZURE_DEVOPS_ORG || 'TBD'}
|
|
482
|
+
- Project: ${projectName}
|
|
483
|
+
- URL: https://dev.azure.com/${process.env.AZURE_DEVOPS_ORG || 'org'}/${projectName}
|
|
484
|
+
|
|
485
|
+
## Specifications
|
|
486
|
+
|
|
487
|
+
_No specifications yet. Specs will appear here as they are created._
|
|
488
|
+
|
|
489
|
+
## Team
|
|
490
|
+
|
|
491
|
+
- Lead: TBD
|
|
492
|
+
- Members: TBD
|
|
493
|
+
|
|
494
|
+
## Keywords
|
|
495
|
+
|
|
496
|
+
${PROJECT_KEYWORDS[projectName]?.join(', ') || 'TBD'}
|
|
497
|
+
|
|
498
|
+
## Getting Started
|
|
499
|
+
|
|
500
|
+
1. Create a new spec: \`/specweave:increment "feature-name"\`
|
|
501
|
+
2. Specs will be organized here automatically
|
|
502
|
+
3. Sync to Azure DevOps: \`/specweave-ado:sync-spec spec-001\`
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
_Generated by SpecWeave_
|
|
507
|
+
`;
|
|
508
|
+
|
|
509
|
+
await fs.writeFile(readmePath, content);
|
|
510
|
+
}
|