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.
- package/CLAUDE.md +21 -0
- package/README.md +10 -9
- package/bin/specweave.js +2 -1
- package/dist/src/cli/commands/archive.d.ts.map +1 -1
- package/dist/src/cli/commands/archive.js +2 -1
- package/dist/src/cli/commands/archive.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +33 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/ado-area-selector.d.ts +49 -0
- package/dist/src/cli/helpers/ado-area-selector.d.ts.map +1 -0
- package/dist/src/cli/helpers/ado-area-selector.js +161 -0
- package/dist/src/cli/helpers/ado-area-selector.js.map +1 -0
- package/dist/src/cli/helpers/init/config-detection.d.ts +5 -1
- package/dist/src/cli/helpers/init/config-detection.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/config-detection.js +50 -17
- package/dist/src/cli/helpers/init/config-detection.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +1 -1
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/init/repository-setup.d.ts +2 -0
- package/dist/src/cli/helpers/init/repository-setup.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/repository-setup.js +34 -2
- package/dist/src/cli/helpers/init/repository-setup.js.map +1 -1
- package/dist/src/cli/helpers/init/types.d.ts +3 -0
- package/dist/src/cli/helpers/init/types.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/ado.d.ts +10 -0
- package/dist/src/cli/helpers/issue-tracker/ado.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/ado.js +107 -22
- package/dist/src/cli/helpers/issue-tracker/ado.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +30 -10
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.d.ts +1 -0
- package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
- package/dist/src/core/increment/discipline-checker.js +1 -1
- package/dist/src/core/increment/increment-archiver.d.ts +13 -0
- package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
- package/dist/src/core/increment/increment-archiver.js +60 -3
- package/dist/src/core/increment/increment-archiver.js.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.js +75 -37
- package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts +2 -111
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +18 -383
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/file-utils.d.ts +30 -0
- package/dist/src/core/living-docs/sync-helpers/file-utils.d.ts.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/file-utils.js +107 -0
- package/dist/src/core/living-docs/sync-helpers/file-utils.js.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts +19 -0
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/generators.js +146 -0
- package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/index.d.ts +8 -0
- package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/index.js +11 -0
- package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/parsers.d.ts +19 -0
- package/dist/src/core/living-docs/sync-helpers/parsers.d.ts.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/parsers.js +94 -0
- package/dist/src/core/living-docs/sync-helpers/parsers.js.map +1 -0
- package/dist/src/core/living-docs/types.d.ts +45 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/types/config.d.ts +1 -1
- package/dist/src/core/types/config.js +2 -2
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/importers/ado-importer.d.ts.map +1 -1
- package/dist/src/importers/ado-importer.js +2 -0
- package/dist/src/importers/ado-importer.js.map +1 -1
- package/dist/src/importers/item-converter.d.ts.map +1 -1
- package/dist/src/importers/item-converter.js +10 -2
- package/dist/src/importers/item-converter.js.map +1 -1
- package/dist/src/living-docs/fs-id-allocator.d.ts +5 -0
- package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
- package/dist/src/living-docs/fs-id-allocator.js +31 -2
- package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
- package/dist/src/utils/external-resource-validator.d.ts +5 -192
- package/dist/src/utils/external-resource-validator.d.ts.map +1 -1
- package/dist/src/utils/external-resource-validator.js +10 -1162
- package/dist/src/utils/external-resource-validator.js.map +1 -1
- package/dist/src/utils/validators/ado-validator.d.ts +86 -0
- package/dist/src/utils/validators/ado-validator.d.ts.map +1 -0
- package/dist/src/utils/validators/ado-validator.js +528 -0
- package/dist/src/utils/validators/ado-validator.js.map +1 -0
- package/dist/src/utils/validators/index.d.ts +11 -0
- package/dist/src/utils/validators/index.d.ts.map +1 -0
- package/dist/src/utils/validators/index.js +12 -0
- package/dist/src/utils/validators/index.js.map +1 -0
- package/dist/src/utils/validators/jira-validator.d.ts +70 -0
- package/dist/src/utils/validators/jira-validator.d.ts.map +1 -0
- package/dist/src/utils/validators/jira-validator.js +606 -0
- package/dist/src/utils/validators/jira-validator.js.map +1 -0
- package/dist/src/utils/validators/types.d.ts +82 -0
- package/dist/src/utils/validators/types.d.ts.map +1 -0
- package/dist/src/utils/validators/types.js +6 -0
- package/dist/src/utils/validators/types.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +7 -62
- package/plugins/specweave/commands/specweave-archive.md +3 -3
- package/plugins/specweave/commands/specweave-increment.md +18 -19
- package/plugins/specweave/hooks/hooks.json +3 -49
- package/plugins/specweave/hooks/hooks.json.bak +72 -0
- package/plugins/specweave/hooks/hooks.json.v1-backup +16 -0
- package/plugins/specweave/hooks/lib/update-status-line.sh +39 -15
- package/plugins/specweave/hooks/post-task-edit.sh +10 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +27 -8
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +44 -0
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +24 -0
- package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +46 -0
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +54 -0
- package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +46 -0
- package/plugins/specweave/hooks/v2/handlers/status-update.sh +50 -0
- package/plugins/specweave/hooks/v2/hooks.json +16 -0
- package/plugins/specweave/hooks/v2/queue/dequeue.sh +30 -0
- package/plugins/specweave/hooks/v2/queue/enqueue.sh +41 -0
- package/plugins/specweave/hooks/v2/queue/processor.sh +72 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +0 -1
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +20 -1262
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +30 -1254
- package/src/templates/tasks.md.template +2 -0
- package/plugins/specweave/hooks/docs-changed.sh.backup +0 -79
- package/plugins/specweave/hooks/human-input-required.sh.backup +0 -75
- package/plugins/specweave/hooks/post-first-increment.sh.backup +0 -61
- package/plugins/specweave/hooks/post-increment-change.sh.backup +0 -98
- package/plugins/specweave/hooks/post-increment-completion.sh.backup +0 -231
- package/plugins/specweave/hooks/post-increment-planning.sh.backup +0 -1048
- package/plugins/specweave/hooks/post-increment-status-change.sh.backup +0 -147
- package/plugins/specweave/hooks/post-spec-update.sh.backup +0 -158
- package/plugins/specweave/hooks/post-user-story-complete.sh.backup +0 -179
- package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +0 -83
- package/plugins/specweave/hooks/pre-implementation.sh.backup +0 -67
- package/plugins/specweave/hooks/pre-task-completion.sh.backup +0 -194
- package/plugins/specweave/hooks/pre-tool-use.sh.backup +0 -133
- package/plugins/specweave/hooks/user-prompt-submit.sh.backup +0 -386
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +0 -353
- package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +0 -172
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -170
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +0 -258
- package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +0 -172
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|