specweave 0.32.5 → 0.32.7
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/README.md +3 -0
- package/bin/specweave.js +78 -0
- package/dist/src/cli/commands/docs.d.ts +45 -0
- package/dist/src/cli/commands/docs.d.ts.map +1 -0
- package/dist/src/cli/commands/docs.js +307 -0
- package/dist/src/cli/commands/docs.js.map +1 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +16 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/bitbucket-repo-cloning.d.ts +42 -0
- package/dist/src/cli/helpers/init/bitbucket-repo-cloning.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/bitbucket-repo-cloning.js +256 -0
- package/dist/src/cli/helpers/init/bitbucket-repo-cloning.js.map +1 -0
- package/dist/src/cli/helpers/init/github-repo-cloning.d.ts +40 -0
- package/dist/src/cli/helpers/init/github-repo-cloning.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/github-repo-cloning.js +284 -0
- package/dist/src/cli/helpers/init/github-repo-cloning.js.map +1 -0
- package/dist/src/cli/helpers/init/repository-setup.d.ts +38 -2
- package/dist/src/cli/helpers/init/repository-setup.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/repository-setup.js +485 -187
- package/dist/src/cli/helpers/init/repository-setup.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/github.d.ts +35 -0
- package/dist/src/cli/helpers/issue-tracker/github.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/github.js +203 -0
- package/dist/src/cli/helpers/issue-tracker/github.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 +127 -13
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/jira.d.ts +18 -2
- package/dist/src/cli/helpers/issue-tracker/jira.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/jira.js +331 -32
- package/dist/src/cli/helpers/issue-tracker/jira.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js +61 -1
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/types.d.ts +25 -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/living-docs/intelligent-analyzer/organization-synthesizer.js +1 -1
- package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.js +12 -16
- package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.js +24 -5
- package/dist/src/utils/docs-preview/docusaurus-setup.js.map +1 -1
- package/dist/src/utils/docs-preview/package-installer.d.ts +3 -0
- package/dist/src/utils/docs-preview/package-installer.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/package-installer.js +18 -9
- package/dist/src/utils/docs-preview/package-installer.js.map +1 -1
- package/dist/src/utils/docs-preview/server-manager.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/server-manager.js +2 -1
- package/dist/src/utils/docs-preview/server-manager.js.map +1 -1
- package/dist/src/utils/validators/jira-validator.d.ts +37 -1
- package/dist/src/utils/validators/jira-validator.d.ts.map +1 -1
- package/dist/src/utils/validators/jira-validator.js +289 -183
- package/dist/src/utils/validators/jira-validator.js.map +1 -1
- package/package.json +3 -3
- package/plugins/specweave/commands/specweave-done.md +34 -0
- package/plugins/specweave/commands/specweave-increment.md +13 -5
- package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +3 -1
- 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/.specweave/logs/hooks-debug.log +0 -1262
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +0 -258
- package/plugins/specweave-github/lib/enhanced-github-sync.js +0 -220
- package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +0 -172
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +0 -134
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1254
- package/plugins/specweave-release/hooks/post-task-completion.sh.backup +0 -110
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* Validates and creates Jira resources (projects, boards).
|
|
5
5
|
* Split from external-resource-validator.ts for maintainability.
|
|
6
6
|
*
|
|
7
|
+
* NEW (v0.33.0): Reads configuration from config.json (ADR-0050 compliant)
|
|
8
|
+
* - Secrets (API token, email) from .env
|
|
9
|
+
* - Configuration (domain, strategy, projects, boards) from config.json
|
|
10
|
+
*
|
|
7
11
|
* @module utils/validators/jira-validator
|
|
8
12
|
*/
|
|
9
13
|
import * as fs from '../fs-native.js';
|
|
14
|
+
import * as path from 'path';
|
|
10
15
|
import { select, input } from '@inquirer/prompts';
|
|
11
16
|
import chalk from 'chalk';
|
|
12
17
|
import { exec } from 'child_process';
|
|
@@ -15,14 +20,18 @@ const execAsync = promisify(exec);
|
|
|
15
20
|
export class JiraResourceValidator {
|
|
16
21
|
constructor(envPath = '.env') {
|
|
17
22
|
this.envPath = envPath;
|
|
18
|
-
//
|
|
23
|
+
// Derive project root from envPath (e.g., '/path/to/project/.env' -> '/path/to/project')
|
|
24
|
+
this.projectRoot = path.dirname(path.resolve(envPath));
|
|
25
|
+
// Load secrets from .env (API token, email)
|
|
19
26
|
const env = this.loadEnv();
|
|
20
27
|
this.apiToken = env.JIRA_API_TOKEN || '';
|
|
21
28
|
this.email = env.JIRA_EMAIL || '';
|
|
22
|
-
|
|
29
|
+
// Load domain from config.json (ADR-0050: non-secrets in config.json)
|
|
30
|
+
const config = this.loadConfig();
|
|
31
|
+
this.domain = config.issueTracker?.domain || env.JIRA_DOMAIN || '';
|
|
23
32
|
}
|
|
24
33
|
/**
|
|
25
|
-
* Load .env file
|
|
34
|
+
* Load .env file (secrets only)
|
|
26
35
|
*/
|
|
27
36
|
loadEnv() {
|
|
28
37
|
try {
|
|
@@ -45,6 +54,48 @@ export class JiraResourceValidator {
|
|
|
45
54
|
return {};
|
|
46
55
|
}
|
|
47
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Load config.json (ADR-0050: non-secrets configuration)
|
|
59
|
+
*
|
|
60
|
+
* NEW (v0.33.0): Configuration is read from config.json, NOT .env
|
|
61
|
+
* This aligns JIRA with ADO init pattern.
|
|
62
|
+
*/
|
|
63
|
+
loadConfig() {
|
|
64
|
+
try {
|
|
65
|
+
const configPath = path.join(this.projectRoot, '.specweave', 'config.json');
|
|
66
|
+
if (!fs.existsSync(configPath)) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Update config.json with new values (for project/board IDs after validation)
|
|
78
|
+
*/
|
|
79
|
+
async updateConfig(updates) {
|
|
80
|
+
try {
|
|
81
|
+
const configPath = path.join(this.projectRoot, '.specweave', 'config.json');
|
|
82
|
+
let config = {};
|
|
83
|
+
if (fs.existsSync(configPath)) {
|
|
84
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
85
|
+
config = JSON.parse(content);
|
|
86
|
+
}
|
|
87
|
+
// Deep merge updates
|
|
88
|
+
if (updates.issueTracker) {
|
|
89
|
+
config.issueTracker = { ...config.issueTracker, ...updates.issueTracker };
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
92
|
+
console.log(chalk.green(`✅ Updated .specweave/config.json`));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error(chalk.red(`❌ Failed to update config.json: ${error.message}`));
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
48
99
|
/**
|
|
49
100
|
* Update .env file with new values
|
|
50
101
|
*/
|
|
@@ -74,9 +125,16 @@ export class JiraResourceValidator {
|
|
|
74
125
|
}
|
|
75
126
|
/**
|
|
76
127
|
* Call Jira API
|
|
128
|
+
*
|
|
129
|
+
* @param endpoint - API endpoint (relative path)
|
|
130
|
+
* @param method - HTTP method (GET, POST, PUT, etc.)
|
|
131
|
+
* @param body - Request body (for POST/PUT)
|
|
132
|
+
* @param apiType - API type: 'rest' for /rest/api/3, 'agile' for /rest/agile/1.0
|
|
77
133
|
*/
|
|
78
|
-
async callJiraApi(endpoint, method = 'GET', body) {
|
|
79
|
-
|
|
134
|
+
async callJiraApi(endpoint, method = 'GET', body, apiType = 'rest') {
|
|
135
|
+
// Choose base path based on API type
|
|
136
|
+
const basePath = apiType === 'agile' ? '/rest/agile/1.0' : '/rest/api/3';
|
|
137
|
+
const url = `https://${this.domain}${basePath}/${endpoint}`;
|
|
80
138
|
const auth = Buffer.from(`${this.email}:${this.apiToken}`).toString('base64');
|
|
81
139
|
const curlCommand = `curl -s -f -X ${method} \
|
|
82
140
|
-H "Authorization: Basic ${auth}" \
|
|
@@ -172,10 +230,12 @@ export class JiraResourceValidator {
|
|
|
172
230
|
}
|
|
173
231
|
/**
|
|
174
232
|
* Fetch all boards for a project
|
|
233
|
+
*
|
|
234
|
+
* Uses Agile API: GET /rest/agile/1.0/board?projectKeyOrId={projectKey}
|
|
175
235
|
*/
|
|
176
236
|
async fetchBoards(projectKey) {
|
|
177
237
|
try {
|
|
178
|
-
const response = await this.callJiraApi(`board?projectKeyOrId=${projectKey}
|
|
238
|
+
const response = await this.callJiraApi(`board?projectKeyOrId=${projectKey}`, 'GET', undefined, 'agile');
|
|
179
239
|
return response.values.map((b) => ({
|
|
180
240
|
id: b.id,
|
|
181
241
|
name: b.name,
|
|
@@ -188,14 +248,17 @@ export class JiraResourceValidator {
|
|
|
188
248
|
}
|
|
189
249
|
/**
|
|
190
250
|
* Check if board exists by ID
|
|
251
|
+
*
|
|
252
|
+
* Uses Agile API: GET /rest/agile/1.0/board/{boardId}
|
|
253
|
+
* Also fetches board configuration to get project information.
|
|
191
254
|
*/
|
|
192
255
|
async checkBoard(boardId) {
|
|
193
256
|
try {
|
|
194
|
-
const board = await this.callJiraApi(`board/${boardId}
|
|
257
|
+
const board = await this.callJiraApi(`board/${boardId}`, 'GET', undefined, 'agile');
|
|
195
258
|
// Fetch board configuration to get project information
|
|
196
259
|
let location;
|
|
197
260
|
try {
|
|
198
|
-
const config = await this.callJiraApi(`board/${boardId}/configuration
|
|
261
|
+
const config = await this.callJiraApi(`board/${boardId}/configuration`, 'GET', undefined, 'agile');
|
|
199
262
|
if (config.location) {
|
|
200
263
|
location = {
|
|
201
264
|
projectKey: config.location.projectKey,
|
|
@@ -220,6 +283,8 @@ export class JiraResourceValidator {
|
|
|
220
283
|
}
|
|
221
284
|
/**
|
|
222
285
|
* Create new Jira board
|
|
286
|
+
*
|
|
287
|
+
* Uses Agile API: POST /rest/agile/1.0/board
|
|
223
288
|
*/
|
|
224
289
|
async createBoard(boardName, projectKey) {
|
|
225
290
|
console.log(chalk.blue(`📦 Creating Jira board: ${boardName} in project ${projectKey}...`));
|
|
@@ -233,7 +298,7 @@ export class JiraResourceValidator {
|
|
|
233
298
|
},
|
|
234
299
|
};
|
|
235
300
|
try {
|
|
236
|
-
const board = await this.callJiraApi('board', 'POST', body);
|
|
301
|
+
const board = await this.callJiraApi('board', 'POST', body, 'agile');
|
|
237
302
|
console.log(chalk.green(`✅ Board created: ${boardName} (ID: ${board.id})`));
|
|
238
303
|
return {
|
|
239
304
|
id: board.id,
|
|
@@ -266,6 +331,14 @@ export class JiraResourceValidator {
|
|
|
266
331
|
}
|
|
267
332
|
/**
|
|
268
333
|
* Validate and fix Jira configuration
|
|
334
|
+
*
|
|
335
|
+
* NEW (v0.33.0): Reads configuration from config.json (ADR-0050 compliant)
|
|
336
|
+
* - Secrets (API token, email) from .env
|
|
337
|
+
* - Configuration (domain, strategy, projects, boards) from config.json
|
|
338
|
+
*
|
|
339
|
+
* Structure levels:
|
|
340
|
+
* - 1-level: project only (issueTracker.projects[].key)
|
|
341
|
+
* - 2-level: project + boards (issueTracker.projects[].boards[])
|
|
269
342
|
*/
|
|
270
343
|
async validate() {
|
|
271
344
|
console.log(chalk.blue('\n🔍 Validating Jira configuration...\n'));
|
|
@@ -275,44 +348,53 @@ export class JiraResourceValidator {
|
|
|
275
348
|
boards: { valid: true, existing: [], missing: [], created: [] },
|
|
276
349
|
envUpdated: false,
|
|
277
350
|
};
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
351
|
+
// Load configuration from config.json (ADR-0050)
|
|
352
|
+
const config = this.loadConfig();
|
|
353
|
+
const issueTracker = config.issueTracker;
|
|
354
|
+
if (!issueTracker || issueTracker.provider !== 'jira') {
|
|
355
|
+
console.log(chalk.red('❌ Jira not configured in .specweave/config.json'));
|
|
356
|
+
console.log(chalk.gray(' Expected: issueTracker.provider = "jira"'));
|
|
357
|
+
console.log(chalk.gray(' Run: specweave init to configure Jira'));
|
|
358
|
+
result.valid = false;
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
const strategy = issueTracker.strategy || 'project-per-team';
|
|
362
|
+
// Determine project key(s) from config.json (NOT .env!)
|
|
281
363
|
let projectKeys = [];
|
|
282
|
-
if (
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
projectKeys = projectsEnv.split(',').map(p => p.trim()).filter(p => p);
|
|
364
|
+
if (issueTracker.projects && issueTracker.projects.length > 0) {
|
|
365
|
+
// NEW: Read from config.json issueTracker.projects array
|
|
366
|
+
projectKeys = issueTracker.projects.map(p => p.key);
|
|
367
|
+
}
|
|
368
|
+
else if (issueTracker.project) {
|
|
369
|
+
// Single project (component-based or board-based strategy)
|
|
370
|
+
projectKeys = [issueTracker.project];
|
|
291
371
|
}
|
|
292
372
|
else {
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
373
|
+
// Fallback: Try to read from sync profiles
|
|
374
|
+
const syncProfiles = config.sync?.profiles;
|
|
375
|
+
if (syncProfiles) {
|
|
376
|
+
for (const profile of Object.values(syncProfiles)) {
|
|
377
|
+
if (profile.config?.projects) {
|
|
378
|
+
projectKeys = profile.config.projects;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
else if (profile.config?.projectKey) {
|
|
382
|
+
projectKeys = [profile.config.projectKey];
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
299
386
|
}
|
|
300
|
-
|
|
387
|
+
}
|
|
388
|
+
if (projectKeys.length === 0) {
|
|
389
|
+
console.log(chalk.red('❌ No Jira projects configured in .specweave/config.json'));
|
|
390
|
+
console.log(chalk.gray(' Expected: issueTracker.projects[].key or issueTracker.project'));
|
|
391
|
+
console.log(chalk.gray(' Run: specweave init to configure Jira projects'));
|
|
392
|
+
result.valid = false;
|
|
393
|
+
return result;
|
|
301
394
|
}
|
|
302
395
|
// 1. Validate project(s)
|
|
303
396
|
console.log(chalk.gray(`Strategy: ${strategy}`));
|
|
304
397
|
console.log(chalk.gray(`Checking project(s): ${projectKeys.join(', ')}...\n`));
|
|
305
|
-
// NEW: Validate per-project var naming (detect orphaned configs)
|
|
306
|
-
const perProjectBoardVars = Object.keys(env).filter(key => key.startsWith('JIRA_BOARDS_'));
|
|
307
|
-
for (const varName of perProjectBoardVars) {
|
|
308
|
-
const projectFromVar = varName.split('JIRA_BOARDS_')[1];
|
|
309
|
-
if (!projectKeys.includes(projectFromVar)) {
|
|
310
|
-
console.log(chalk.yellow(`⚠️ Configuration warning: ${varName}`));
|
|
311
|
-
console.log(chalk.gray(` Project "${projectFromVar}" not found in JIRA_PROJECTS`));
|
|
312
|
-
console.log(chalk.gray(` Expected projects: ${projectKeys.join(', ')}`));
|
|
313
|
-
console.log(chalk.gray(` This configuration will be ignored.\n`));
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
398
|
// Track all validated/created projects (for multi-project IDs)
|
|
317
399
|
const allProjects = [];
|
|
318
400
|
for (const projectKey of projectKeys) {
|
|
@@ -353,14 +435,24 @@ export class JiraResourceValidator {
|
|
|
353
435
|
console.log(chalk.red(`❌ Failed to fetch details for project "${selectedProject}"\n`));
|
|
354
436
|
continue;
|
|
355
437
|
}
|
|
356
|
-
// Update .
|
|
438
|
+
// Update config.json (handle both single and multiple projects)
|
|
439
|
+
// ADR-0050: Non-secrets (project keys) go in config.json, not .env
|
|
357
440
|
if (strategy === 'project-per-team') {
|
|
358
|
-
// Replace this project
|
|
359
|
-
const
|
|
360
|
-
|
|
441
|
+
// Replace this project in issueTracker.projects array
|
|
442
|
+
const config = this.loadConfig();
|
|
443
|
+
const existingProjectsList = config.issueTracker?.projects || [];
|
|
444
|
+
const updatedProjects = existingProjectsList.map(p => p.key === projectKey
|
|
445
|
+
? { ...p, key: selectedProject, id: selectedProjectDetails.id, name: selectedProjectDetails.name }
|
|
446
|
+
: p);
|
|
447
|
+
await this.updateConfig({
|
|
448
|
+
issueTracker: { projects: updatedProjects }
|
|
449
|
+
});
|
|
361
450
|
}
|
|
362
451
|
else {
|
|
363
|
-
|
|
452
|
+
// Single project mode - update issueTracker.project
|
|
453
|
+
await this.updateConfig({
|
|
454
|
+
issueTracker: { project: selectedProject }
|
|
455
|
+
});
|
|
364
456
|
}
|
|
365
457
|
// Print link to selected project
|
|
366
458
|
const projectUrl = `https://${this.domain}/jira/software/c/projects/${selectedProject}`;
|
|
@@ -423,172 +515,186 @@ export class JiraResourceValidator {
|
|
|
423
515
|
}
|
|
424
516
|
}
|
|
425
517
|
console.log(); // Empty line after project validation
|
|
426
|
-
// Update .
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
await this.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
for (const projectKey of projectKeys) {
|
|
444
|
-
const perProjectKey = `JIRA_BOARDS_${projectKey}`;
|
|
445
|
-
if (env[perProjectKey]) {
|
|
446
|
-
hasPerProjectBoards = true;
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
518
|
+
// Update config.json with project IDs (for multi-project strategy)
|
|
519
|
+
// NOTE: In v0.33.0+, we update config.json, NOT .env (ADR-0050)
|
|
520
|
+
if (allProjects.length > 0) {
|
|
521
|
+
// Update issueTracker.projects with validated IDs
|
|
522
|
+
const updatedProjects = allProjects.map(p => ({
|
|
523
|
+
key: p.key,
|
|
524
|
+
id: p.id,
|
|
525
|
+
name: p.name
|
|
526
|
+
}));
|
|
527
|
+
await this.updateConfig({
|
|
528
|
+
issueTracker: {
|
|
529
|
+
...issueTracker,
|
|
530
|
+
projects: updatedProjects
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
result.envUpdated = true; // Renamed but still indicates config was updated
|
|
534
|
+
console.log(chalk.green(`✅ Updated config.json with project IDs\n`));
|
|
449
535
|
}
|
|
536
|
+
// 2. Validate boards (from config.json issueTracker.projects[].boards)
|
|
537
|
+
result.boards = { valid: true, existing: [], missing: [], created: [] };
|
|
538
|
+
// Check if any project has boards configured in config.json
|
|
539
|
+
const projectsWithBoards = issueTracker.projects?.filter(p => p.boards && p.boards.length > 0) || [];
|
|
540
|
+
const hasPerProjectBoards = projectsWithBoards.length > 0;
|
|
541
|
+
// Also check legacy board-based strategy (issueTracker.boards)
|
|
542
|
+
const legacyBoards = issueTracker.boards || [];
|
|
450
543
|
if (hasPerProjectBoards) {
|
|
451
|
-
// Per-project boards (
|
|
544
|
+
// Per-project boards from config.json (2-level structure)
|
|
452
545
|
console.log(chalk.gray(`Checking per-project boards...\n`));
|
|
453
546
|
// Track board names to detect conflicts across projects
|
|
454
547
|
const boardNamesSeen = new Map(); // name -> project
|
|
455
|
-
for (const
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
if (
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
548
|
+
for (const projectConfig of issueTracker.projects || []) {
|
|
549
|
+
const projectKey = projectConfig.key;
|
|
550
|
+
const boards = projectConfig.boards || [];
|
|
551
|
+
if (boards.length > 0) {
|
|
552
|
+
console.log(chalk.gray(` Project: ${projectKey} (${boards.length} boards)`));
|
|
553
|
+
const finalBoardIds = [];
|
|
554
|
+
for (const boardConfig of boards) {
|
|
555
|
+
const boardIdStr = boardConfig.id;
|
|
556
|
+
const isNumeric = /^\d+$/.test(boardIdStr);
|
|
557
|
+
if (isNumeric) {
|
|
558
|
+
// Entry is a board ID - validate it exists AND belongs to this project
|
|
559
|
+
const boardId = parseInt(boardIdStr, 10);
|
|
560
|
+
const board = await this.checkBoard(boardId);
|
|
561
|
+
if (board) {
|
|
562
|
+
// Validate board belongs to the correct project
|
|
563
|
+
if (board.location?.projectKey && board.location.projectKey !== projectKey) {
|
|
564
|
+
console.log(chalk.yellow(` ⚠️ Board ${boardId}: ${board.name} belongs to project ${board.location.projectKey}, not ${projectKey}`));
|
|
565
|
+
console.log(chalk.gray(` Expected: ${projectKey}, Found: ${board.location.projectKey}`));
|
|
566
|
+
result.boards.missing.push(boardIdStr);
|
|
567
|
+
result.boards.valid = false;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// Board exists and belongs to correct project
|
|
571
|
+
if (board.location?.projectKey) {
|
|
572
|
+
console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (project: ${board.location.projectKey})`));
|
|
476
573
|
}
|
|
477
574
|
else {
|
|
478
|
-
|
|
479
|
-
if (board.location?.projectKey) {
|
|
480
|
-
console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (project: ${board.location.projectKey})`));
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (project verification skipped)`));
|
|
484
|
-
}
|
|
485
|
-
result.boards.existing.push(board.id);
|
|
486
|
-
finalBoardIds.push(board.id);
|
|
575
|
+
console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (project verification skipped)`));
|
|
487
576
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
console.log(chalk.yellow(` ⚠️ Board ${boardId}: Not found`));
|
|
491
|
-
result.boards.missing.push(entry);
|
|
492
|
-
result.boards.valid = false;
|
|
577
|
+
result.boards.existing.push(board.id);
|
|
578
|
+
finalBoardIds.push({ id: String(board.id), name: board.name });
|
|
493
579
|
}
|
|
494
580
|
}
|
|
495
581
|
else {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
582
|
+
console.log(chalk.yellow(` ⚠️ Board ${boardId}: Not found`));
|
|
583
|
+
result.boards.missing.push(boardIdStr);
|
|
584
|
+
result.boards.valid = false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
// Entry is a board name - check for conflicts, then create it
|
|
589
|
+
const boardName = boardConfig.name || boardIdStr;
|
|
590
|
+
if (boardNamesSeen.has(boardName)) {
|
|
591
|
+
const existingProject = boardNamesSeen.get(boardName);
|
|
592
|
+
console.log(chalk.yellow(` ⚠️ Board name conflict: "${boardName}" already used in project ${existingProject}`));
|
|
593
|
+
console.log(chalk.gray(` Tip: Use unique board names or append project suffix (e.g., "${boardName}-${projectKey}")`));
|
|
594
|
+
result.boards.missing.push(boardName);
|
|
595
|
+
result.boards.valid = false;
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
console.log(chalk.blue(` 📦 Creating board: ${boardName}...`));
|
|
599
|
+
try {
|
|
600
|
+
const board = await this.createBoard(boardName, projectKey);
|
|
601
|
+
console.log(chalk.green(` ✅ Created: ${boardName} (ID: ${board.id})`));
|
|
602
|
+
result.boards.created.push({ name: boardName, id: board.id });
|
|
603
|
+
finalBoardIds.push({ id: String(board.id), name: board.name });
|
|
604
|
+
boardNamesSeen.set(boardName, projectKey);
|
|
504
605
|
}
|
|
505
|
-
|
|
506
|
-
console.log(chalk.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
console.log(chalk.green(` ✅ Created: ${entry} (ID: ${board.id})`));
|
|
510
|
-
result.boards.created.push({ name: entry, id: board.id });
|
|
511
|
-
finalBoardIds.push(board.id);
|
|
512
|
-
boardNamesSeen.set(entry, projectKey); // Track this board name
|
|
513
|
-
}
|
|
514
|
-
catch (error) {
|
|
515
|
-
console.log(chalk.red(` ❌ Failed to create ${entry}: ${error.message}`));
|
|
516
|
-
result.boards.missing.push(entry);
|
|
517
|
-
result.boards.valid = false;
|
|
518
|
-
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
console.log(chalk.red(` ❌ Failed to create ${boardName}: ${error.message}`));
|
|
608
|
+
result.boards.missing.push(boardName);
|
|
609
|
+
result.boards.valid = false;
|
|
519
610
|
}
|
|
520
611
|
}
|
|
521
612
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
613
|
+
}
|
|
614
|
+
// Update config.json with validated board IDs
|
|
615
|
+
if (finalBoardIds.length > 0) {
|
|
616
|
+
const currentConfig = this.loadConfig();
|
|
617
|
+
const updatedProjects = (currentConfig.issueTracker?.projects || []).map(p => {
|
|
618
|
+
if (p.key === projectKey) {
|
|
619
|
+
return { ...p, boards: finalBoardIds };
|
|
620
|
+
}
|
|
621
|
+
return p;
|
|
622
|
+
});
|
|
623
|
+
await this.updateConfig({
|
|
624
|
+
issueTracker: {
|
|
625
|
+
...currentConfig.issueTracker,
|
|
626
|
+
projects: updatedProjects
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
result.envUpdated = true;
|
|
630
|
+
console.log(chalk.green(` ✅ Updated config.json with board IDs for ${projectKey}`));
|
|
528
631
|
}
|
|
529
632
|
}
|
|
530
633
|
}
|
|
531
634
|
console.log();
|
|
532
635
|
}
|
|
533
|
-
else {
|
|
534
|
-
// Legacy: Global boards (backward compatibility)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
result.boards.existing.push(board.id);
|
|
551
|
-
finalBoardIds.push(board.id);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
console.log(chalk.yellow(` ⚠️ Board ${boardId}: Not found`));
|
|
555
|
-
result.boards.missing.push(entry);
|
|
556
|
-
result.boards.valid = false;
|
|
557
|
-
}
|
|
636
|
+
else if (legacyBoards.length > 0 && strategy === 'board-based') {
|
|
637
|
+
// Legacy: Global boards from config.json (backward compatibility)
|
|
638
|
+
console.log(chalk.gray(`Checking boards: ${legacyBoards.map(b => b.id).join(', ')}...`));
|
|
639
|
+
// For board-based strategy, use the single project key
|
|
640
|
+
const projectKeyForBoards = projectKeys[0];
|
|
641
|
+
const finalBoardIds = [];
|
|
642
|
+
for (const boardConfig of legacyBoards) {
|
|
643
|
+
const boardIdStr = boardConfig.id;
|
|
644
|
+
const isNumeric = /^\d+$/.test(boardIdStr);
|
|
645
|
+
if (isNumeric) {
|
|
646
|
+
// Entry is a board ID - validate it exists
|
|
647
|
+
const boardId = parseInt(boardIdStr, 10);
|
|
648
|
+
const board = await this.checkBoard(boardId);
|
|
649
|
+
if (board) {
|
|
650
|
+
console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (exists)`));
|
|
651
|
+
result.boards.existing.push(board.id);
|
|
652
|
+
finalBoardIds.push({ id: String(board.id), name: board.name });
|
|
558
653
|
}
|
|
559
654
|
else {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const board = await this.createBoard(entry, projectKeyForBoards);
|
|
564
|
-
console.log(chalk.green(` ✅ Created: ${entry} (ID: ${board.id})`));
|
|
565
|
-
result.boards.created.push({ name: entry, id: board.id });
|
|
566
|
-
finalBoardIds.push(board.id);
|
|
567
|
-
}
|
|
568
|
-
catch (error) {
|
|
569
|
-
console.log(chalk.red(` ❌ Failed to create ${entry}: ${error.message}`));
|
|
570
|
-
result.boards.missing.push(entry);
|
|
571
|
-
result.boards.valid = false;
|
|
572
|
-
}
|
|
655
|
+
console.log(chalk.yellow(` ⚠️ Board ${boardId}: Not found`));
|
|
656
|
+
result.boards.missing.push(boardIdStr);
|
|
657
|
+
result.boards.valid = false;
|
|
573
658
|
}
|
|
574
659
|
}
|
|
575
|
-
// Update .env if any boards were created
|
|
576
|
-
if (result.boards.created.length > 0) {
|
|
577
|
-
console.log(chalk.blue('\n📝 Updating .env with board IDs...'));
|
|
578
|
-
await this.updateEnv({ JIRA_BOARDS: finalBoardIds.join(',') });
|
|
579
|
-
result.boards.existing = finalBoardIds;
|
|
580
|
-
result.envUpdated = true;
|
|
581
|
-
console.log(chalk.green(`✅ Updated JIRA_BOARDS: ${finalBoardIds.join(',')}`));
|
|
582
|
-
}
|
|
583
|
-
// Summary
|
|
584
|
-
console.log();
|
|
585
|
-
if (result.boards.missing.length > 0) {
|
|
586
|
-
console.log(chalk.yellow(`⚠️ Issues found: ${result.boards.missing.length} board(s)\n`));
|
|
587
|
-
}
|
|
588
660
|
else {
|
|
589
|
-
|
|
661
|
+
// Entry is a board name - create it
|
|
662
|
+
const boardName = boardConfig.name || boardIdStr;
|
|
663
|
+
console.log(chalk.blue(` 📦 Creating board: ${boardName}...`));
|
|
664
|
+
try {
|
|
665
|
+
const board = await this.createBoard(boardName, projectKeyForBoards);
|
|
666
|
+
console.log(chalk.green(` ✅ Created: ${boardName} (ID: ${board.id})`));
|
|
667
|
+
result.boards.created.push({ name: boardName, id: board.id });
|
|
668
|
+
finalBoardIds.push({ id: String(board.id), name: board.name });
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
console.log(chalk.red(` ❌ Failed to create ${boardName}: ${error.message}`));
|
|
672
|
+
result.boards.missing.push(boardName);
|
|
673
|
+
result.boards.valid = false;
|
|
674
|
+
}
|
|
590
675
|
}
|
|
591
676
|
}
|
|
677
|
+
// Update config.json if any boards were validated/created
|
|
678
|
+
if (finalBoardIds.length > 0) {
|
|
679
|
+
console.log(chalk.blue('\n📝 Updating config.json with board IDs...'));
|
|
680
|
+
await this.updateConfig({
|
|
681
|
+
issueTracker: {
|
|
682
|
+
...issueTracker,
|
|
683
|
+
boards: finalBoardIds
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
result.boards.existing = finalBoardIds.map(b => parseInt(b.id, 10));
|
|
687
|
+
result.envUpdated = true;
|
|
688
|
+
console.log(chalk.green(`✅ Updated issueTracker.boards in config.json`));
|
|
689
|
+
}
|
|
690
|
+
// Summary
|
|
691
|
+
console.log();
|
|
692
|
+
if (result.boards.missing.length > 0) {
|
|
693
|
+
console.log(chalk.yellow(`⚠️ Issues found: ${result.boards.missing.length} board(s)\n`));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
console.log(chalk.green(`✅ All boards validated/created successfully\n`));
|
|
697
|
+
}
|
|
592
698
|
}
|
|
593
699
|
return result;
|
|
594
700
|
}
|