specweave 0.28.36 → 0.28.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CLAUDE.md +21 -0
  2. package/README.md +10 -9
  3. package/bin/specweave.js +2 -1
  4. package/dist/src/cli/commands/archive.d.ts.map +1 -1
  5. package/dist/src/cli/commands/archive.js +2 -1
  6. package/dist/src/cli/commands/archive.js.map +1 -1
  7. package/dist/src/cli/commands/init.d.ts.map +1 -1
  8. package/dist/src/cli/commands/init.js +33 -0
  9. package/dist/src/cli/commands/init.js.map +1 -1
  10. package/dist/src/cli/helpers/ado-area-selector.d.ts +49 -0
  11. package/dist/src/cli/helpers/ado-area-selector.d.ts.map +1 -0
  12. package/dist/src/cli/helpers/ado-area-selector.js +161 -0
  13. package/dist/src/cli/helpers/ado-area-selector.js.map +1 -0
  14. package/dist/src/cli/helpers/init/config-detection.d.ts +5 -1
  15. package/dist/src/cli/helpers/init/config-detection.d.ts.map +1 -1
  16. package/dist/src/cli/helpers/init/config-detection.js +50 -17
  17. package/dist/src/cli/helpers/init/config-detection.js.map +1 -1
  18. package/dist/src/cli/helpers/init/external-import.js +1 -1
  19. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  20. package/dist/src/cli/helpers/init/repository-setup.d.ts +2 -0
  21. package/dist/src/cli/helpers/init/repository-setup.d.ts.map +1 -1
  22. package/dist/src/cli/helpers/init/repository-setup.js +34 -2
  23. package/dist/src/cli/helpers/init/repository-setup.js.map +1 -1
  24. package/dist/src/cli/helpers/init/types.d.ts +3 -0
  25. package/dist/src/cli/helpers/init/types.d.ts.map +1 -1
  26. package/dist/src/cli/helpers/issue-tracker/ado.d.ts +10 -0
  27. package/dist/src/cli/helpers/issue-tracker/ado.d.ts.map +1 -1
  28. package/dist/src/cli/helpers/issue-tracker/ado.js +107 -22
  29. package/dist/src/cli/helpers/issue-tracker/ado.js.map +1 -1
  30. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  31. package/dist/src/cli/helpers/issue-tracker/index.js +30 -10
  32. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  33. package/dist/src/cli/helpers/issue-tracker/types.d.ts +1 -0
  34. package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
  35. package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
  36. package/dist/src/core/increment/discipline-checker.js +1 -1
  37. package/dist/src/core/increment/increment-archiver.d.ts +13 -0
  38. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
  39. package/dist/src/core/increment/increment-archiver.js +60 -3
  40. package/dist/src/core/increment/increment-archiver.js.map +1 -1
  41. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
  42. package/dist/src/core/living-docs/feature-archiver.js +75 -37
  43. package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
  44. package/dist/src/core/living-docs/living-docs-sync.d.ts +2 -111
  45. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  46. package/dist/src/core/living-docs/living-docs-sync.js +18 -383
  47. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  48. package/dist/src/core/living-docs/sync-helpers/file-utils.d.ts +30 -0
  49. package/dist/src/core/living-docs/sync-helpers/file-utils.d.ts.map +1 -0
  50. package/dist/src/core/living-docs/sync-helpers/file-utils.js +107 -0
  51. package/dist/src/core/living-docs/sync-helpers/file-utils.js.map +1 -0
  52. package/dist/src/core/living-docs/sync-helpers/generators.d.ts +19 -0
  53. package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -0
  54. package/dist/src/core/living-docs/sync-helpers/generators.js +146 -0
  55. package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -0
  56. package/dist/src/core/living-docs/sync-helpers/index.d.ts +8 -0
  57. package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -0
  58. package/dist/src/core/living-docs/sync-helpers/index.js +11 -0
  59. package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -0
  60. package/dist/src/core/living-docs/sync-helpers/parsers.d.ts +19 -0
  61. package/dist/src/core/living-docs/sync-helpers/parsers.d.ts.map +1 -0
  62. package/dist/src/core/living-docs/sync-helpers/parsers.js +94 -0
  63. package/dist/src/core/living-docs/sync-helpers/parsers.js.map +1 -0
  64. package/dist/src/core/living-docs/types.d.ts +45 -0
  65. package/dist/src/core/living-docs/types.d.ts.map +1 -1
  66. package/dist/src/core/types/config.d.ts +1 -1
  67. package/dist/src/core/types/config.js +2 -2
  68. package/dist/src/core/types/config.js.map +1 -1
  69. package/dist/src/importers/ado-importer.d.ts.map +1 -1
  70. package/dist/src/importers/ado-importer.js +2 -0
  71. package/dist/src/importers/ado-importer.js.map +1 -1
  72. package/dist/src/importers/item-converter.d.ts.map +1 -1
  73. package/dist/src/importers/item-converter.js +10 -2
  74. package/dist/src/importers/item-converter.js.map +1 -1
  75. package/dist/src/living-docs/fs-id-allocator.d.ts +5 -0
  76. package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
  77. package/dist/src/living-docs/fs-id-allocator.js +31 -2
  78. package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
  79. package/dist/src/utils/external-resource-validator.d.ts +5 -192
  80. package/dist/src/utils/external-resource-validator.d.ts.map +1 -1
  81. package/dist/src/utils/external-resource-validator.js +10 -1162
  82. package/dist/src/utils/external-resource-validator.js.map +1 -1
  83. package/dist/src/utils/validators/ado-validator.d.ts +86 -0
  84. package/dist/src/utils/validators/ado-validator.d.ts.map +1 -0
  85. package/dist/src/utils/validators/ado-validator.js +528 -0
  86. package/dist/src/utils/validators/ado-validator.js.map +1 -0
  87. package/dist/src/utils/validators/index.d.ts +11 -0
  88. package/dist/src/utils/validators/index.d.ts.map +1 -0
  89. package/dist/src/utils/validators/index.js +12 -0
  90. package/dist/src/utils/validators/index.js.map +1 -0
  91. package/dist/src/utils/validators/jira-validator.d.ts +70 -0
  92. package/dist/src/utils/validators/jira-validator.d.ts.map +1 -0
  93. package/dist/src/utils/validators/jira-validator.js +606 -0
  94. package/dist/src/utils/validators/jira-validator.js.map +1 -0
  95. package/dist/src/utils/validators/types.d.ts +82 -0
  96. package/dist/src/utils/validators/types.d.ts.map +1 -0
  97. package/dist/src/utils/validators/types.js +6 -0
  98. package/dist/src/utils/validators/types.js.map +1 -0
  99. package/package.json +1 -1
  100. package/plugins/specweave/.claude-plugin/plugin.json +7 -62
  101. package/plugins/specweave/commands/specweave-archive.md +3 -3
  102. package/plugins/specweave/commands/specweave-increment.md +18 -19
  103. package/plugins/specweave/hooks/hooks.json +3 -49
  104. package/plugins/specweave/hooks/hooks.json.bak +72 -0
  105. package/plugins/specweave/hooks/hooks.json.v1-backup +16 -0
  106. package/plugins/specweave/hooks/lib/update-status-line.sh +39 -15
  107. package/plugins/specweave/hooks/post-task-edit.sh +10 -0
  108. package/plugins/specweave/hooks/user-prompt-submit.sh +27 -8
  109. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +44 -0
  110. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +24 -0
  111. package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +46 -0
  112. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +54 -0
  113. package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +46 -0
  114. package/plugins/specweave/hooks/v2/handlers/status-update.sh +50 -0
  115. package/plugins/specweave/hooks/v2/hooks.json +16 -0
  116. package/plugins/specweave/hooks/v2/queue/dequeue.sh +30 -0
  117. package/plugins/specweave/hooks/v2/queue/enqueue.sh +41 -0
  118. package/plugins/specweave/hooks/v2/queue/processor.sh +72 -0
  119. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +0 -1
  120. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +20 -1262
  121. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
  122. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +30 -1254
  123. package/src/templates/tasks.md.template +2 -0
  124. package/plugins/specweave/hooks/docs-changed.sh.backup +0 -79
  125. package/plugins/specweave/hooks/human-input-required.sh.backup +0 -75
  126. package/plugins/specweave/hooks/post-first-increment.sh.backup +0 -61
  127. package/plugins/specweave/hooks/post-increment-change.sh.backup +0 -98
  128. package/plugins/specweave/hooks/post-increment-completion.sh.backup +0 -231
  129. package/plugins/specweave/hooks/post-increment-planning.sh.backup +0 -1048
  130. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +0 -147
  131. package/plugins/specweave/hooks/post-spec-update.sh.backup +0 -158
  132. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +0 -179
  133. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +0 -83
  134. package/plugins/specweave/hooks/pre-implementation.sh.backup +0 -67
  135. package/plugins/specweave/hooks/pre-task-completion.sh.backup +0 -194
  136. package/plugins/specweave/hooks/pre-tool-use.sh.backup +0 -133
  137. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +0 -386
  138. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +0 -353
  139. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +0 -172
  140. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -170
  141. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +0 -258
  142. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +0 -172
  143. package/plugins/specweave-release/hooks/post-task-completion.sh.backup +0 -110
@@ -1,1170 +1,18 @@
1
1
  /**
2
2
  * External Resource Validator
3
3
  *
4
- * Validates and creates external resources (Jira projects, boards, etc.)
5
- * Smart enough to:
6
- * - Check if resources exist
7
- * - Prompt user to select existing or create new
8
- * - Create missing resources automatically
9
- * - Update .env with actual IDs after creation
4
+ * Re-exports from validators/ for backward compatibility.
5
+ * The actual implementations are in:
6
+ * - validators/jira-validator.ts
7
+ * - validators/ado-validator.ts
10
8
  *
11
9
  * @module utils/external-resource-validator
12
10
  * @since 0.9.5
13
11
  */
14
- import * as fs from '../utils/fs-native.js';
15
- import { select, input } from '@inquirer/prompts';
16
- import chalk from 'chalk';
17
- import { exec } from 'child_process';
18
- import { promisify } from 'util';
19
- const execAsync = promisify(exec);
20
- // ============================================================================
21
- // Jira Resource Validator
22
- // ============================================================================
23
- export class JiraResourceValidator {
24
- constructor(envPath = '.env') {
25
- this.envPath = envPath;
26
- // Load from .env
27
- const env = this.loadEnv();
28
- this.apiToken = env.JIRA_API_TOKEN || '';
29
- this.email = env.JIRA_EMAIL || '';
30
- this.domain = env.JIRA_DOMAIN || '';
31
- }
32
- /**
33
- * Load .env file
34
- */
35
- loadEnv() {
36
- try {
37
- if (!fs.existsSync(this.envPath)) {
38
- return {};
39
- }
40
- const content = fs.readFileSync(this.envPath, 'utf-8');
41
- const env = {};
42
- content.split('\n').forEach((line) => {
43
- const match = line.match(/^([^=:#]+)=(.*)$/);
44
- if (match) {
45
- const key = match[1].trim();
46
- const value = match[2].trim();
47
- env[key] = value;
48
- }
49
- });
50
- return env;
51
- }
52
- catch (error) {
53
- return {};
54
- }
55
- }
56
- /**
57
- * Update .env file with new values
58
- */
59
- async updateEnv(updates) {
60
- try {
61
- let content = '';
62
- if (fs.existsSync(this.envPath)) {
63
- content = fs.readFileSync(this.envPath, 'utf-8');
64
- }
65
- // Update existing or append new
66
- Object.entries(updates).forEach(([key, value]) => {
67
- const regex = new RegExp(`^${key}=.*$`, 'm');
68
- if (regex.test(content)) {
69
- content = content.replace(regex, `${key}=${value}`);
70
- }
71
- else {
72
- content += `\n${key}=${value}`;
73
- }
74
- });
75
- fs.writeFileSync(this.envPath, content.trim() + '\n');
76
- console.log(chalk.green(`✅ Updated ${this.envPath}`));
77
- }
78
- catch (error) {
79
- console.error(chalk.red(`❌ Failed to update ${this.envPath}: ${error.message}`));
80
- throw error;
81
- }
82
- }
83
- /**
84
- * Call Jira API
85
- */
86
- async callJiraApi(endpoint, method = 'GET', body) {
87
- const url = `https://${this.domain}/rest/api/3/${endpoint}`;
88
- const auth = Buffer.from(`${this.email}:${this.apiToken}`).toString('base64');
89
- const curlCommand = `curl -s -f -X ${method} \
90
- -H "Authorization: Basic ${auth}" \
91
- -H "Content-Type: application/json" \
92
- ${body ? `-d '${JSON.stringify(body)}'` : ''} \
93
- "${url}"`;
94
- try {
95
- const { stdout } = await execAsync(curlCommand);
96
- const response = JSON.parse(stdout);
97
- // Double-check for error response (defense in depth)
98
- if (response.errorMessages || response.errors) {
99
- const errorMsg = response.errorMessages?.join(', ') || JSON.stringify(response.errors);
100
- throw new Error(errorMsg);
101
- }
102
- return response;
103
- }
104
- catch (error) {
105
- // Improve error message for common cases
106
- if (error.message.includes('curl: (22)')) {
107
- throw new Error('Resource not found (HTTP 404)');
108
- }
109
- throw error;
110
- }
111
- }
112
- /**
113
- * Fetch all Jira projects
114
- */
115
- async fetchProjects() {
116
- try {
117
- const response = await this.callJiraApi('project');
118
- return response.map((p) => ({
119
- id: p.id,
120
- key: p.key,
121
- name: p.name,
122
- }));
123
- }
124
- catch (error) {
125
- return [];
126
- }
127
- }
128
- /**
129
- * Check if project exists
130
- */
131
- async checkProject(projectKey) {
132
- try {
133
- const project = await this.callJiraApi(`project/${projectKey}`);
134
- return {
135
- id: project.id,
136
- key: project.key,
137
- name: project.name,
138
- };
139
- }
140
- catch (error) {
141
- return null;
142
- }
143
- }
144
- /**
145
- * Create new Jira project
146
- */
147
- async createProject(projectKey, projectName) {
148
- console.log(chalk.blue(`📦 Creating Jira project: ${projectKey} (${projectName})...`));
149
- const body = {
150
- key: projectKey,
151
- name: projectName,
152
- projectTypeKey: 'software',
153
- leadAccountId: await this.getCurrentUserId(),
154
- };
155
- try {
156
- const project = await this.callJiraApi('project', 'POST', body);
157
- console.log(chalk.green(`✅ Project created: ${projectKey}`));
158
- return {
159
- id: project.id,
160
- key: project.key,
161
- name: project.name,
162
- };
163
- }
164
- catch (error) {
165
- console.error(chalk.red(`❌ Failed to create project: ${error.message}`));
166
- throw error;
167
- }
168
- }
169
- /**
170
- * Get current user ID (for project lead)
171
- */
172
- async getCurrentUserId() {
173
- try {
174
- const user = await this.callJiraApi('myself');
175
- return user.accountId;
176
- }
177
- catch (error) {
178
- throw new Error('Failed to get current user ID');
179
- }
180
- }
181
- /**
182
- * Fetch all boards for a project
183
- */
184
- async fetchBoards(projectKey) {
185
- try {
186
- const response = await this.callJiraApi(`board?projectKeyOrId=${projectKey}`);
187
- return response.values.map((b) => ({
188
- id: b.id,
189
- name: b.name,
190
- type: b.type,
191
- }));
192
- }
193
- catch (error) {
194
- return [];
195
- }
196
- }
197
- /**
198
- * Check if board exists by ID
199
- */
200
- async checkBoard(boardId) {
201
- try {
202
- const board = await this.callJiraApi(`board/${boardId}`);
203
- // Fetch board configuration to get project information
204
- let location;
205
- try {
206
- const config = await this.callJiraApi(`board/${boardId}/configuration`);
207
- if (config.location) {
208
- location = {
209
- projectKey: config.location.projectKey,
210
- projectId: config.location.projectId,
211
- };
212
- }
213
- }
214
- catch (error) {
215
- // Configuration fetch failed, board exists but we don't know which project
216
- // This is OK for backward compatibility
217
- }
218
- return {
219
- id: board.id,
220
- name: board.name,
221
- type: board.type,
222
- location,
223
- };
224
- }
225
- catch (error) {
226
- return null;
227
- }
228
- }
229
- /**
230
- * Create new Jira board
231
- */
232
- async createBoard(boardName, projectKey) {
233
- console.log(chalk.blue(`📦 Creating Jira board: ${boardName} in project ${projectKey}...`));
234
- const body = {
235
- name: boardName,
236
- type: 'scrum',
237
- filterId: await this.getOrCreateFilter(projectKey),
238
- location: {
239
- type: 'project',
240
- projectKeyOrId: projectKey,
241
- },
242
- };
243
- try {
244
- const board = await this.callJiraApi('board', 'POST', body);
245
- console.log(chalk.green(`✅ Board created: ${boardName} (ID: ${board.id})`));
246
- return {
247
- id: board.id,
248
- name: board.name,
249
- type: board.type,
250
- };
251
- }
252
- catch (error) {
253
- console.error(chalk.red(`❌ Failed to create board: ${error.message}`));
254
- throw error;
255
- }
256
- }
257
- /**
258
- * Get or create filter for board
259
- */
260
- async getOrCreateFilter(projectKey) {
261
- // For simplicity, create a basic filter
262
- // In production, you might want to check for existing filters first
263
- const body = {
264
- name: `${projectKey} Issues`,
265
- jql: `project = ${projectKey}`,
266
- };
267
- try {
268
- const filter = await this.callJiraApi('filter', 'POST', body);
269
- return filter.id;
270
- }
271
- catch (error) {
272
- throw new Error(`Failed to create filter: ${error.message}`);
273
- }
274
- }
275
- /**
276
- * Validate and fix Jira configuration
277
- */
278
- async validate() {
279
- console.log(chalk.blue('\n🔍 Validating Jira configuration...\n'));
280
- const result = {
281
- valid: true,
282
- project: { exists: false },
283
- boards: { valid: true, existing: [], missing: [], created: [] },
284
- envUpdated: false,
285
- };
286
- const env = this.loadEnv();
287
- const strategy = env.JIRA_STRATEGY || 'project-per-team';
288
- // Determine project key(s) based on strategy
289
- let projectKeys = [];
290
- if (strategy === 'project-per-team') {
291
- // Multiple projects (JIRA_PROJECTS is comma-separated)
292
- const projectsEnv = env.JIRA_PROJECTS || '';
293
- if (!projectsEnv) {
294
- console.log(chalk.red('❌ JIRA_PROJECTS not found in .env'));
295
- result.valid = false;
296
- return result;
297
- }
298
- projectKeys = projectsEnv.split(',').map(p => p.trim()).filter(p => p);
299
- }
300
- else {
301
- // Single project (component-based or board-based)
302
- const projectKey = env.JIRA_PROJECT;
303
- if (!projectKey) {
304
- console.log(chalk.red('❌ JIRA_PROJECT not found in .env'));
305
- result.valid = false;
306
- return result;
307
- }
308
- projectKeys = [projectKey];
309
- }
310
- // 1. Validate project(s)
311
- console.log(chalk.gray(`Strategy: ${strategy}`));
312
- console.log(chalk.gray(`Checking project(s): ${projectKeys.join(', ')}...\n`));
313
- // NEW: Validate per-project var naming (detect orphaned configs)
314
- const perProjectBoardVars = Object.keys(env).filter(key => key.startsWith('JIRA_BOARDS_'));
315
- for (const varName of perProjectBoardVars) {
316
- const projectFromVar = varName.split('JIRA_BOARDS_')[1];
317
- if (!projectKeys.includes(projectFromVar)) {
318
- console.log(chalk.yellow(`⚠️ Configuration warning: ${varName}`));
319
- console.log(chalk.gray(` Project "${projectFromVar}" not found in JIRA_PROJECTS`));
320
- console.log(chalk.gray(` Expected projects: ${projectKeys.join(', ')}`));
321
- console.log(chalk.gray(` This configuration will be ignored.\n`));
322
- }
323
- }
324
- // Track all validated/created projects (for multi-project IDs)
325
- const allProjects = [];
326
- for (const projectKey of projectKeys) {
327
- const project = await this.checkProject(projectKey);
328
- if (!project) {
329
- console.log(chalk.yellow(`⚠️ Project "${projectKey}" not found\n`));
330
- // Fetch existing projects
331
- const existingProjects = await this.fetchProjects();
332
- // Prompt user
333
- const action = await select({
334
- message: `What would you like to do for project "${projectKey}"?`,
335
- choices: [
336
- { name: 'Select an existing project', value: 'select' },
337
- { name: 'Create a new project', value: 'create' },
338
- { name: 'Skip this project', value: 'skip' },
339
- { name: 'Cancel validation', value: 'cancel' },
340
- ],
341
- });
342
- if (action === 'cancel') {
343
- result.valid = false;
344
- return result;
345
- }
346
- if (action === 'skip') {
347
- console.log(chalk.yellow(`⏭️ Skipped project "${projectKey}"\n`));
348
- continue;
349
- }
350
- if (action === 'select') {
351
- const selectedProject = await select({
352
- message: 'Select a project:',
353
- choices: existingProjects.map((p) => ({
354
- name: `${p.key} - ${p.name}`,
355
- value: p.key,
356
- })),
357
- });
358
- // Fetch full project details to get ID
359
- const selectedProjectDetails = await this.checkProject(selectedProject);
360
- if (!selectedProjectDetails) {
361
- console.log(chalk.red(`❌ Failed to fetch details for project "${selectedProject}"\n`));
362
- continue;
363
- }
364
- // Update .env (handle both single and multiple projects)
365
- if (strategy === 'project-per-team') {
366
- // Replace this project key in JIRA_PROJECTS
367
- const updatedKeys = projectKeys.map(k => k === projectKey ? selectedProject : k);
368
- await this.updateEnv({ JIRA_PROJECTS: updatedKeys.join(',') });
369
- }
370
- else {
371
- await this.updateEnv({ JIRA_PROJECT: selectedProject });
372
- }
373
- // Print link to selected project
374
- const projectUrl = `https://${this.domain}/jira/software/c/projects/${selectedProject}`;
375
- console.log(chalk.cyan(`🔗 View in Jira: ${projectUrl}`));
376
- result.project = {
377
- exists: true,
378
- key: selectedProject,
379
- id: selectedProjectDetails.id,
380
- name: selectedProjectDetails.name,
381
- };
382
- result.envUpdated = true;
383
- console.log(chalk.green(`✅ Project "${selectedProject}" selected\n`));
384
- // Track for multi-project ID collection
385
- allProjects.push({
386
- key: selectedProject,
387
- id: selectedProjectDetails.id,
388
- name: selectedProjectDetails.name,
389
- });
390
- }
391
- else if (action === 'create') {
392
- const projectName = await input({
393
- message: 'Enter project name:',
394
- default: projectKey,
395
- });
396
- const newProject = await this.createProject(projectKey, projectName);
397
- // Print link to created project
398
- const projectUrl = `https://${this.domain}/jira/software/c/projects/${newProject.key}`;
399
- console.log(chalk.cyan(`🔗 View in Jira: ${projectUrl}\n`));
400
- result.project = {
401
- exists: true,
402
- key: newProject.key,
403
- id: newProject.id,
404
- name: newProject.name,
405
- };
406
- // Track for multi-project ID collection
407
- allProjects.push({
408
- key: newProject.key,
409
- id: newProject.id,
410
- name: newProject.name,
411
- });
412
- }
413
- }
414
- else {
415
- console.log(chalk.green(`✅ Validated: Project "${projectKey}" exists in Jira`));
416
- // Print link to validated project
417
- const projectUrl = `https://${this.domain}/jira/software/c/projects/${project.key}`;
418
- console.log(chalk.cyan(`🔗 View in Jira: ${projectUrl}`));
419
- result.project = {
420
- exists: true,
421
- key: project.key,
422
- id: project.id,
423
- name: project.name,
424
- };
425
- // Track for multi-project ID collection
426
- allProjects.push({
427
- key: project.key,
428
- id: project.id,
429
- name: project.name,
430
- });
431
- }
432
- }
433
- console.log(); // Empty line after project validation
434
- // Update .env with project IDs (for multi-project strategy)
435
- if (strategy === 'project-per-team' && allProjects.length > 0) {
436
- const projectIds = allProjects.map(p => p.id).join(',');
437
- await this.updateEnv({ JIRA_PROJECT_IDS: projectIds });
438
- result.envUpdated = true;
439
- console.log(chalk.green(`✅ Updated .env with project IDs: ${projectIds}\n`));
440
- }
441
- else if (allProjects.length === 1) {
442
- // Single project - store both key and ID
443
- await this.updateEnv({ JIRA_PROJECT_ID: allProjects[0].id });
444
- result.envUpdated = true;
445
- console.log(chalk.green(`✅ Updated .env with project ID: ${allProjects[0].id}\n`));
446
- }
447
- // 2. Validate boards (per-project OR legacy board-based strategy)
448
- result.boards = { valid: true, existing: [], missing: [], created: [] };
449
- // NEW: Check for per-project boards (JIRA_BOARDS_{ProjectKey})
450
- let hasPerProjectBoards = false;
451
- for (const projectKey of projectKeys) {
452
- const perProjectKey = `JIRA_BOARDS_${projectKey}`;
453
- if (env[perProjectKey]) {
454
- hasPerProjectBoards = true;
455
- break;
456
- }
457
- }
458
- if (hasPerProjectBoards) {
459
- // Per-project boards (NEW!)
460
- console.log(chalk.gray(`Checking per-project boards...\n`));
461
- // Track board names to detect conflicts across projects
462
- const boardNamesSeen = new Map(); // name -> project
463
- for (const projectKey of projectKeys) {
464
- const perProjectKey = `JIRA_BOARDS_${projectKey}`;
465
- const boardsConfig = env[perProjectKey];
466
- if (boardsConfig) {
467
- const boardEntries = boardsConfig.split(',').map((b) => b.trim()).filter(b => b);
468
- if (boardEntries.length > 0) {
469
- console.log(chalk.gray(` Project: ${projectKey} (${boardEntries.length} boards)`));
470
- const finalBoardIds = [];
471
- for (const entry of boardEntries) {
472
- const isNumeric = /^\d+$/.test(entry);
473
- if (isNumeric) {
474
- // Entry is a board ID - validate it exists AND belongs to this project
475
- const boardId = parseInt(entry, 10);
476
- const board = await this.checkBoard(boardId);
477
- if (board) {
478
- // NEW: Validate board belongs to the correct project
479
- if (board.location?.projectKey && board.location.projectKey !== projectKey) {
480
- console.log(chalk.yellow(` ⚠️ Board ${boardId}: ${board.name} belongs to project ${board.location.projectKey}, not ${projectKey}`));
481
- console.log(chalk.gray(` Expected: ${projectKey}, Found: ${board.location.projectKey}`));
482
- result.boards.missing.push(entry);
483
- result.boards.valid = false;
484
- }
485
- else {
486
- // Board exists and belongs to correct project (or project unknown - backward compat)
487
- if (board.location?.projectKey) {
488
- console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (project: ${board.location.projectKey})`));
489
- }
490
- else {
491
- console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (project verification skipped)`));
492
- }
493
- result.boards.existing.push(board.id);
494
- finalBoardIds.push(board.id);
495
- }
496
- }
497
- else {
498
- console.log(chalk.yellow(` ⚠️ Board ${boardId}: Not found`));
499
- result.boards.missing.push(entry);
500
- result.boards.valid = false;
501
- }
502
- }
503
- else {
504
- // Entry is a board name - check for conflicts, then create it
505
- // NEW: Detect board name conflicts across projects
506
- if (boardNamesSeen.has(entry)) {
507
- const existingProject = boardNamesSeen.get(entry);
508
- console.log(chalk.yellow(` ⚠️ Board name conflict: "${entry}" already used in project ${existingProject}`));
509
- console.log(chalk.gray(` Tip: Use unique board names or append project suffix (e.g., "${entry}-${projectKey}")`));
510
- result.boards.missing.push(entry);
511
- result.boards.valid = false;
512
- }
513
- else {
514
- console.log(chalk.blue(` 📦 Creating board: ${entry}...`));
515
- try {
516
- const board = await this.createBoard(entry, projectKey);
517
- console.log(chalk.green(` ✅ Created: ${entry} (ID: ${board.id})`));
518
- result.boards.created.push({ name: entry, id: board.id });
519
- finalBoardIds.push(board.id);
520
- boardNamesSeen.set(entry, projectKey); // Track this board name
521
- }
522
- catch (error) {
523
- console.log(chalk.red(` ❌ Failed to create ${entry}: ${error.message}`));
524
- result.boards.missing.push(entry);
525
- result.boards.valid = false;
526
- }
527
- }
528
- }
529
- }
530
- // Update .env with final board IDs for this project
531
- if (finalBoardIds.length > 0) {
532
- await this.updateEnv({ [perProjectKey]: finalBoardIds.join(',') });
533
- result.envUpdated = true;
534
- console.log(chalk.green(` ✅ Updated ${perProjectKey}: ${finalBoardIds.join(',')}`));
535
- }
536
- }
537
- }
538
- }
539
- console.log();
540
- }
541
- else {
542
- // Legacy: Global boards (backward compatibility)
543
- const boardsConfig = env.JIRA_BOARDS || '';
544
- if (boardsConfig && strategy === 'board-based') {
545
- console.log(chalk.gray(`Checking boards: ${boardsConfig}...`));
546
- // For board-based strategy, use the single project key
547
- const projectKeyForBoards = projectKeys[0];
548
- const boardEntries = boardsConfig.split(',').map((b) => b.trim());
549
- const finalBoardIds = [];
550
- for (const entry of boardEntries) {
551
- const isNumeric = /^\d+$/.test(entry);
552
- if (isNumeric) {
553
- // Entry is a board ID - validate it exists
554
- const boardId = parseInt(entry, 10);
555
- const board = await this.checkBoard(boardId);
556
- if (board) {
557
- console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (exists)`));
558
- result.boards.existing.push(board.id);
559
- finalBoardIds.push(board.id);
560
- }
561
- else {
562
- console.log(chalk.yellow(` ⚠️ Board ${boardId}: Not found`));
563
- result.boards.missing.push(entry);
564
- result.boards.valid = false;
565
- }
566
- }
567
- else {
568
- // Entry is a board name - create it
569
- console.log(chalk.blue(` 📦 Creating board: ${entry}...`));
570
- try {
571
- const board = await this.createBoard(entry, projectKeyForBoards);
572
- console.log(chalk.green(` ✅ Created: ${entry} (ID: ${board.id})`));
573
- result.boards.created.push({ name: entry, id: board.id });
574
- finalBoardIds.push(board.id);
575
- }
576
- catch (error) {
577
- console.log(chalk.red(` ❌ Failed to create ${entry}: ${error.message}`));
578
- result.boards.missing.push(entry);
579
- result.boards.valid = false;
580
- }
581
- }
582
- }
583
- // Update .env if any boards were created
584
- if (result.boards.created.length > 0) {
585
- console.log(chalk.blue('\n📝 Updating .env with board IDs...'));
586
- await this.updateEnv({ JIRA_BOARDS: finalBoardIds.join(',') });
587
- result.boards.existing = finalBoardIds;
588
- result.envUpdated = true;
589
- console.log(chalk.green(`✅ Updated JIRA_BOARDS: ${finalBoardIds.join(',')}`));
590
- }
591
- // Summary
592
- console.log();
593
- if (result.boards.missing.length > 0) {
594
- console.log(chalk.yellow(`⚠️ Issues found: ${result.boards.missing.length} board(s)\n`));
595
- }
596
- else {
597
- console.log(chalk.green(`✅ All boards validated/created successfully\n`));
598
- }
599
- }
600
- }
601
- return result;
602
- }
603
- }
604
- export class AzureDevOpsResourceValidator {
605
- constructor(envPath = '.env') {
606
- this.envPath = envPath;
607
- // Load from .env
608
- const env = this.loadEnv();
609
- this.pat = env.AZURE_DEVOPS_PAT || '';
610
- this.organization = env.AZURE_DEVOPS_ORG || '';
611
- }
612
- /**
613
- * Load .env file
614
- */
615
- loadEnv() {
616
- try {
617
- if (!fs.existsSync(this.envPath)) {
618
- return {};
619
- }
620
- const content = fs.readFileSync(this.envPath, 'utf-8');
621
- const env = {};
622
- content.split('\n').forEach((line) => {
623
- const match = line.match(/^([^=:#]+)=(.*)$/);
624
- if (match) {
625
- const key = match[1].trim();
626
- const value = match[2].trim();
627
- env[key] = value;
628
- }
629
- });
630
- return env;
631
- }
632
- catch (error) {
633
- return {};
634
- }
635
- }
636
- /**
637
- * Update .env file with new values
638
- */
639
- async updateEnv(updates) {
640
- try {
641
- let content = '';
642
- if (fs.existsSync(this.envPath)) {
643
- content = fs.readFileSync(this.envPath, 'utf-8');
644
- }
645
- // Update existing or append new
646
- Object.entries(updates).forEach(([key, value]) => {
647
- const regex = new RegExp(`^${key}=.*$`, 'm');
648
- if (regex.test(content)) {
649
- content = content.replace(regex, `${key}=${value}`);
650
- }
651
- else {
652
- content += `\n${key}=${value}`;
653
- }
654
- });
655
- fs.writeFileSync(this.envPath, content.trim() + '\n');
656
- console.log(chalk.green(`✅ Updated ${this.envPath}`));
657
- }
658
- catch (error) {
659
- console.error(chalk.red(`❌ Failed to update ${this.envPath}: ${error.message}`));
660
- throw error;
661
- }
662
- }
663
- /**
664
- * Call Azure DevOps API
665
- */
666
- async callAzureDevOpsApi(endpoint, method = 'GET', body) {
667
- const url = `https://dev.azure.com/${this.organization}/_apis/${endpoint}`;
668
- const auth = Buffer.from(`:${this.pat}`).toString('base64');
669
- const curlCommand = `curl -s -f -X ${method} \
670
- -H "Authorization: Basic ${auth}" \
671
- -H "Content-Type: application/json" \
672
- ${body ? `-d '${JSON.stringify(body)}'` : ''} \
673
- "${url}"`;
674
- try {
675
- const { stdout } = await execAsync(curlCommand);
676
- const response = JSON.parse(stdout);
677
- // Check for error response
678
- if (response.message || response.errorMessage) {
679
- throw new Error(response.message || response.errorMessage);
680
- }
681
- return response;
682
- }
683
- catch (error) {
684
- // Improve error message for common cases
685
- if (error.message.includes('curl: (22)')) {
686
- throw new Error('Resource not found (HTTP 404)');
687
- }
688
- throw error;
689
- }
690
- }
691
- /**
692
- * Fetch all Azure DevOps projects
693
- */
694
- async fetchProjects() {
695
- try {
696
- const response = await this.callAzureDevOpsApi('projects?api-version=7.0');
697
- return response.value.map((p) => ({
698
- id: p.id,
699
- name: p.name,
700
- description: p.description || '',
701
- }));
702
- }
703
- catch (error) {
704
- return [];
705
- }
706
- }
707
- /**
708
- * Check if project exists
709
- */
710
- async checkProject(projectName) {
711
- try {
712
- const project = await this.callAzureDevOpsApi(`projects/${encodeURIComponent(projectName)}?api-version=7.0`);
713
- return {
714
- id: project.id,
715
- name: project.name,
716
- description: project.description || '',
717
- };
718
- }
719
- catch (error) {
720
- return null;
721
- }
722
- }
723
- /**
724
- * Create new Azure DevOps project
725
- */
726
- async createProject(projectName, description = '') {
727
- console.log(chalk.blue(`📦 Creating Azure DevOps project: ${projectName}...`));
728
- const body = {
729
- name: projectName,
730
- description: description || `${projectName} project`,
731
- capabilities: {
732
- versioncontrol: {
733
- sourceControlType: 'Git'
734
- },
735
- processTemplate: {
736
- templateTypeId: 'adcc42ab-9882-485e-a3ed-7678f01f66bc' // Agile process template
737
- }
738
- }
739
- };
740
- try {
741
- const project = await this.callAzureDevOpsApi('projects?api-version=7.0', 'POST', body);
742
- // Wait for project creation to complete (ADO creates projects asynchronously)
743
- await this.waitForProjectCreation(project.id);
744
- console.log(chalk.green(`✅ Project created: ${projectName} (ID: ${project.id})`));
745
- return {
746
- id: project.id,
747
- name: projectName,
748
- description: description,
749
- };
750
- }
751
- catch (error) {
752
- console.error(chalk.red(`❌ Failed to create project: ${error.message}`));
753
- throw error;
754
- }
755
- }
756
- /**
757
- * Wait for project creation to complete
758
- */
759
- async waitForProjectCreation(projectId, maxAttempts = 10) {
760
- for (let i = 0; i < maxAttempts; i++) {
761
- try {
762
- const operation = await this.callAzureDevOpsApi(`operations/${projectId}?api-version=7.0`);
763
- if (operation.status === 'succeeded') {
764
- return;
765
- }
766
- if (operation.status === 'failed') {
767
- throw new Error('Project creation failed');
768
- }
769
- // Wait 2 seconds before next check
770
- await new Promise(resolve => setTimeout(resolve, 2000));
771
- }
772
- catch (error) {
773
- // Operation might not exist yet, continue waiting
774
- await new Promise(resolve => setTimeout(resolve, 2000));
775
- }
776
- }
777
- // Project creation timeout is not fatal - it might still succeed
778
- console.log(chalk.yellow('⚠️ Project creation may still be in progress'));
779
- }
780
- /**
781
- * Create area path in project
782
- */
783
- async createAreaPath(projectName, areaName) {
784
- console.log(chalk.blue(` 📦 Creating area path: ${projectName}\\${areaName}...`));
785
- const body = {
786
- name: areaName
787
- };
788
- try {
789
- const area = await this.callAzureDevOpsApi(`wit/classificationnodes/areas?projectId=${encodeURIComponent(projectName)}&api-version=7.0`, 'POST', body);
790
- console.log(chalk.green(` ✅ Area path created: ${projectName}\\${areaName}`));
791
- return {
792
- id: area.id,
793
- name: area.name,
794
- path: area.path,
795
- };
796
- }
797
- catch (error) {
798
- console.error(chalk.red(` ❌ Failed to create area path: ${error.message}`));
799
- throw error;
800
- }
801
- }
802
- /**
803
- * Fetch teams in project
804
- */
805
- async fetchTeams(projectName) {
806
- try {
807
- const response = await this.callAzureDevOpsApi(`projects/${encodeURIComponent(projectName)}/teams?api-version=7.0`);
808
- return response.value.map((t) => ({
809
- id: t.id,
810
- name: t.name,
811
- description: t.description || '',
812
- }));
813
- }
814
- catch (error) {
815
- return [];
816
- }
817
- }
818
- /**
819
- * Create team in project
820
- */
821
- async createTeam(projectName, teamName) {
822
- console.log(chalk.blue(` 📦 Creating team: ${teamName}...`));
823
- const body = {
824
- name: teamName,
825
- description: `${teamName} development team`
826
- };
827
- try {
828
- const team = await this.callAzureDevOpsApi(`projects/${encodeURIComponent(projectName)}/teams?api-version=7.0`, 'POST', body);
829
- console.log(chalk.green(` ✅ Team created: ${teamName}`));
830
- return {
831
- id: team.id,
832
- name: team.name,
833
- description: team.description || '',
834
- };
835
- }
836
- catch (error) {
837
- console.error(chalk.red(` ❌ Failed to create team: ${error.message}`));
838
- throw error;
839
- }
840
- }
841
- /**
842
- * Validate and fix Azure DevOps configuration
843
- */
844
- async validate() {
845
- console.log(chalk.blue('\n🔍 Validating Azure DevOps configuration...\n'));
846
- const env = this.loadEnv();
847
- const strategy = (env.AZURE_DEVOPS_STRATEGY || 'project-per-team');
848
- const result = {
849
- valid: true,
850
- strategy,
851
- projects: [],
852
- envUpdated: false,
853
- };
854
- // Determine project names based on strategy
855
- let projectNames = [];
856
- if (strategy === 'project-per-team') {
857
- // Multiple projects
858
- const projectsEnv = env.AZURE_DEVOPS_PROJECTS || '';
859
- if (!projectsEnv) {
860
- console.log(chalk.red('❌ AZURE_DEVOPS_PROJECTS not found in .env'));
861
- result.valid = false;
862
- return result;
863
- }
864
- projectNames = projectsEnv.split(',').map(p => p.trim()).filter(p => p);
865
- }
866
- else {
867
- // Single project (area-path-based or team-based)
868
- const projectName = env.AZURE_DEVOPS_PROJECT;
869
- if (!projectName) {
870
- console.log(chalk.red('❌ AZURE_DEVOPS_PROJECT not found in .env'));
871
- result.valid = false;
872
- return result;
873
- }
874
- projectNames = [projectName];
875
- }
876
- console.log(chalk.gray(`Strategy: ${strategy}`));
877
- console.log(chalk.gray(`Checking project(s): ${projectNames.join(', ')}...\n`));
878
- // NEW: Validate per-project var naming (detect orphaned configs)
879
- const perProjectVars = Object.keys(env).filter(key => key.startsWith('AZURE_DEVOPS_AREA_PATHS_') || key.startsWith('AZURE_DEVOPS_TEAMS_'));
880
- for (const varName of perProjectVars) {
881
- const projectFromVar = varName.includes('_AREA_PATHS_')
882
- ? varName.split('_AREA_PATHS_')[1]
883
- : varName.split('_TEAMS_')[1];
884
- if (!projectNames.includes(projectFromVar)) {
885
- console.log(chalk.yellow(`⚠️ Configuration warning: ${varName}`));
886
- console.log(chalk.gray(` Project "${projectFromVar}" not found in AZURE_DEVOPS_PROJECTS`));
887
- console.log(chalk.gray(` Expected projects: ${projectNames.join(', ')}`));
888
- console.log(chalk.gray(` This configuration will be ignored.\n`));
889
- }
890
- }
891
- // 1. Validate projects
892
- for (const projectName of projectNames) {
893
- const project = await this.checkProject(projectName);
894
- if (!project) {
895
- console.log(chalk.yellow(`⚠️ Project "${projectName}" not found\n`));
896
- // Fetch existing projects
897
- const existingProjects = await this.fetchProjects();
898
- // Prompt user
899
- const action = await select({
900
- message: `What would you like to do for project "${projectName}"?`,
901
- choices: [
902
- { name: 'Select an existing project', value: 'select' },
903
- { name: 'Create a new project', value: 'create' },
904
- { name: 'Skip this project', value: 'skip' },
905
- { name: 'Cancel validation', value: 'cancel' },
906
- ],
907
- });
908
- if (action === 'cancel') {
909
- result.valid = false;
910
- return result;
911
- }
912
- if (action === 'skip') {
913
- console.log(chalk.yellow(`⏭️ Skipped project "${projectName}"\n`));
914
- result.projects.push({ name: projectName, exists: false, created: false });
915
- continue;
916
- }
917
- if (action === 'select') {
918
- const selectedProject = await select({
919
- message: 'Select a project:',
920
- choices: existingProjects.map((p) => ({
921
- name: `${p.name}${p.description ? ` - ${p.description}` : ''}`,
922
- value: p.name,
923
- })),
924
- });
925
- // Fetch full project details
926
- const selectedProjectDetails = await this.checkProject(selectedProject);
927
- if (!selectedProjectDetails) {
928
- console.log(chalk.red(`❌ Failed to fetch details for project "${selectedProject}"\n`));
929
- continue;
930
- }
931
- // Update .env
932
- if (strategy === 'project-per-team') {
933
- const updatedNames = projectNames.map(n => n === projectName ? selectedProject : n);
934
- await this.updateEnv({ AZURE_DEVOPS_PROJECTS: updatedNames.join(',') });
935
- }
936
- else {
937
- await this.updateEnv({ AZURE_DEVOPS_PROJECT: selectedProject });
938
- }
939
- const projectUrl = `https://dev.azure.com/${this.organization}/${encodeURIComponent(selectedProject)}`;
940
- console.log(chalk.cyan(`🔗 View in Azure DevOps: ${projectUrl}`));
941
- console.log(chalk.green(`✅ Project "${selectedProject}" selected\n`));
942
- result.projects.push({
943
- name: selectedProject,
944
- id: selectedProjectDetails.id,
945
- exists: true,
946
- created: false,
947
- });
948
- result.envUpdated = true;
949
- }
950
- else if (action === 'create') {
951
- const description = await input({
952
- message: 'Enter project description (optional):',
953
- default: `${projectName} project`,
954
- });
955
- const newProject = await this.createProject(projectName, description);
956
- const projectUrl = `https://dev.azure.com/${this.organization}/${encodeURIComponent(newProject.name)}`;
957
- console.log(chalk.cyan(`🔗 View in Azure DevOps: ${projectUrl}\n`));
958
- result.projects.push({
959
- name: newProject.name,
960
- id: newProject.id,
961
- exists: true,
962
- created: true,
963
- });
964
- }
965
- }
966
- else {
967
- console.log(chalk.green(`✅ Validated: Project "${projectName}" exists`));
968
- const projectUrl = `https://dev.azure.com/${this.organization}/${encodeURIComponent(project.name)}`;
969
- console.log(chalk.cyan(`🔗 View in Azure DevOps: ${projectUrl}`));
970
- result.projects.push({
971
- name: project.name,
972
- id: project.id,
973
- exists: true,
974
- created: false,
975
- });
976
- }
977
- }
978
- console.log(); // Empty line after project validation
979
- // 2. Validate area paths (per-project OR legacy area-path-based strategy)
980
- result.areaPaths = [];
981
- // NEW: Check for per-project area paths (AZURE_DEVOPS_AREA_PATHS_{ProjectName})
982
- let hasPerProjectAreaPaths = false;
983
- for (const projectName of projectNames) {
984
- const perProjectKey = `AZURE_DEVOPS_AREA_PATHS_${projectName}`;
985
- if (env[perProjectKey]) {
986
- hasPerProjectAreaPaths = true;
987
- break;
988
- }
989
- }
990
- if (hasPerProjectAreaPaths) {
991
- // Per-project area paths (NEW!)
992
- console.log(chalk.gray(`Checking per-project area paths...\n`));
993
- for (const projectName of projectNames) {
994
- const perProjectKey = `AZURE_DEVOPS_AREA_PATHS_${projectName}`;
995
- const areaPathsConfig = env[perProjectKey];
996
- if (areaPathsConfig) {
997
- const areaNames = areaPathsConfig.split(',').map(a => a.trim()).filter(a => a);
998
- if (areaNames.length > 0) {
999
- console.log(chalk.gray(` Project: ${projectName} (${areaNames.length} area paths)`));
1000
- for (const areaName of areaNames) {
1001
- try {
1002
- await this.createAreaPath(projectName, areaName);
1003
- result.areaPaths.push({
1004
- name: areaName,
1005
- project: projectName,
1006
- exists: false,
1007
- created: true
1008
- });
1009
- }
1010
- catch (error) {
1011
- if (error.message.includes('already exists')) {
1012
- console.log(chalk.green(` ✅ Area path exists: ${projectName}\\${areaName}`));
1013
- result.areaPaths.push({
1014
- name: areaName,
1015
- project: projectName,
1016
- exists: true,
1017
- created: false
1018
- });
1019
- }
1020
- else {
1021
- console.log(chalk.red(` ❌ Failed to create/validate area path: ${areaName}`));
1022
- result.valid = false;
1023
- }
1024
- }
1025
- }
1026
- }
1027
- }
1028
- }
1029
- console.log();
1030
- }
1031
- else if (strategy === 'area-path-based') {
1032
- // Legacy: Global area paths (backward compatibility)
1033
- const areaPathsConfig = env.AZURE_DEVOPS_AREA_PATHS || '';
1034
- if (areaPathsConfig) {
1035
- console.log(chalk.gray(`Checking area paths...`));
1036
- const projectName = projectNames[0]; // Single project for area-path-based
1037
- const areaNames = areaPathsConfig.split(',').map(a => a.trim());
1038
- for (const areaName of areaNames) {
1039
- // Check if area path exists (simplified - would need proper API call)
1040
- // For now, we'll create them if they don't exist
1041
- try {
1042
- await this.createAreaPath(projectName, areaName);
1043
- result.areaPaths.push({ name: areaName, exists: false, created: true });
1044
- }
1045
- catch (error) {
1046
- if (error.message.includes('already exists')) {
1047
- console.log(chalk.green(` ✅ Area path exists: ${projectName}\\${areaName}`));
1048
- result.areaPaths.push({ name: areaName, exists: true, created: false });
1049
- }
1050
- else {
1051
- console.log(chalk.red(` ❌ Failed to create/validate area path: ${areaName}`));
1052
- result.valid = false;
1053
- }
1054
- }
1055
- }
1056
- console.log();
1057
- }
1058
- }
1059
- // 3. Validate teams (per-project OR legacy team-based strategy)
1060
- result.teams = [];
1061
- // NEW: Check for per-project teams (AZURE_DEVOPS_TEAMS_{ProjectName})
1062
- let hasPerProjectTeams = false;
1063
- for (const projectName of projectNames) {
1064
- const perProjectKey = `AZURE_DEVOPS_TEAMS_${projectName}`;
1065
- if (env[perProjectKey]) {
1066
- hasPerProjectTeams = true;
1067
- break;
1068
- }
1069
- }
1070
- if (hasPerProjectTeams) {
1071
- // Per-project teams (NEW!)
1072
- console.log(chalk.gray(`Checking per-project teams...\n`));
1073
- for (const projectName of projectNames) {
1074
- const perProjectKey = `AZURE_DEVOPS_TEAMS_${projectName}`;
1075
- const teamsConfig = env[perProjectKey];
1076
- if (teamsConfig) {
1077
- const teamNames = teamsConfig.split(',').map(t => t.trim()).filter(t => t);
1078
- if (teamNames.length > 0) {
1079
- console.log(chalk.gray(` Project: ${projectName} (${teamNames.length} teams)`));
1080
- const existingTeams = await this.fetchTeams(projectName);
1081
- for (const teamName of teamNames) {
1082
- const team = existingTeams.find(t => t.name === teamName);
1083
- if (team) {
1084
- console.log(chalk.green(` ✅ Team exists: ${teamName}`));
1085
- result.teams.push({
1086
- name: teamName,
1087
- id: team.id,
1088
- project: projectName,
1089
- exists: true,
1090
- created: false
1091
- });
1092
- }
1093
- else {
1094
- try {
1095
- const newTeam = await this.createTeam(projectName, teamName);
1096
- result.teams.push({
1097
- name: teamName,
1098
- id: newTeam.id,
1099
- project: projectName,
1100
- exists: false,
1101
- created: true
1102
- });
1103
- }
1104
- catch (error) {
1105
- console.log(chalk.red(` ❌ Failed to create team: ${teamName}`));
1106
- result.valid = false;
1107
- }
1108
- }
1109
- }
1110
- }
1111
- }
1112
- }
1113
- console.log();
1114
- }
1115
- else if (strategy === 'team-based') {
1116
- // Legacy: Global teams (backward compatibility)
1117
- const teamsConfig = env.AZURE_DEVOPS_TEAMS || '';
1118
- if (teamsConfig) {
1119
- console.log(chalk.gray(`Checking teams...`));
1120
- const projectName = projectNames[0]; // Single project for team-based
1121
- const teamNames = teamsConfig.split(',').map(t => t.trim());
1122
- const existingTeams = await this.fetchTeams(projectName);
1123
- for (const teamName of teamNames) {
1124
- const team = existingTeams.find(t => t.name === teamName);
1125
- if (team) {
1126
- console.log(chalk.green(` ✅ Team exists: ${teamName}`));
1127
- result.teams.push({ name: teamName, id: team.id, exists: true, created: false });
1128
- }
1129
- else {
1130
- try {
1131
- const newTeam = await this.createTeam(projectName, teamName);
1132
- result.teams.push({ name: teamName, id: newTeam.id, exists: false, created: true });
1133
- }
1134
- catch (error) {
1135
- console.log(chalk.red(` ❌ Failed to create team: ${teamName}`));
1136
- result.valid = false;
1137
- }
1138
- }
1139
- }
1140
- console.log();
1141
- }
1142
- }
1143
- // Summary
1144
- if (result.valid) {
1145
- console.log(chalk.green(`✅ Azure DevOps configuration validated successfully\n`));
1146
- }
1147
- else {
1148
- console.log(chalk.yellow(`⚠️ Some resources could not be validated\n`));
1149
- }
1150
- return result;
1151
- }
1152
- }
1153
- // ============================================================================
1154
- // Utility Functions
1155
- // ============================================================================
1156
- /**
1157
- * Validate Jira resources
1158
- */
1159
- export async function validateJiraResources(envPath = '.env') {
1160
- const validator = new JiraResourceValidator(envPath);
1161
- return validator.validate();
1162
- }
1163
- /**
1164
- * Validate Azure DevOps resources
1165
- */
1166
- export async function validateAzureDevOpsResources(envPath = '.env') {
1167
- const validator = new AzureDevOpsResourceValidator(envPath);
1168
- return validator.validate();
1169
- }
12
+ // Re-export everything from validators/
13
+ export {
14
+ // Classes
15
+ JiraResourceValidator, AzureDevOpsResourceValidator,
16
+ // Utility functions
17
+ validateJiraResources, validateAzureDevOpsResources, } from './validators/index.js';
1170
18
  //# sourceMappingURL=external-resource-validator.js.map