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.
Files changed (84) hide show
  1. package/README.md +3 -0
  2. package/bin/specweave.js +78 -0
  3. package/dist/src/cli/commands/docs.d.ts +45 -0
  4. package/dist/src/cli/commands/docs.d.ts.map +1 -0
  5. package/dist/src/cli/commands/docs.js +307 -0
  6. package/dist/src/cli/commands/docs.js.map +1 -0
  7. package/dist/src/cli/commands/init.d.ts.map +1 -1
  8. package/dist/src/cli/commands/init.js +16 -0
  9. package/dist/src/cli/commands/init.js.map +1 -1
  10. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.d.ts +42 -0
  11. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.d.ts.map +1 -0
  12. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.js +256 -0
  13. package/dist/src/cli/helpers/init/bitbucket-repo-cloning.js.map +1 -0
  14. package/dist/src/cli/helpers/init/github-repo-cloning.d.ts +40 -0
  15. package/dist/src/cli/helpers/init/github-repo-cloning.d.ts.map +1 -0
  16. package/dist/src/cli/helpers/init/github-repo-cloning.js +284 -0
  17. package/dist/src/cli/helpers/init/github-repo-cloning.js.map +1 -0
  18. package/dist/src/cli/helpers/init/repository-setup.d.ts +38 -2
  19. package/dist/src/cli/helpers/init/repository-setup.d.ts.map +1 -1
  20. package/dist/src/cli/helpers/init/repository-setup.js +485 -187
  21. package/dist/src/cli/helpers/init/repository-setup.js.map +1 -1
  22. package/dist/src/cli/helpers/issue-tracker/github.d.ts +35 -0
  23. package/dist/src/cli/helpers/issue-tracker/github.d.ts.map +1 -1
  24. package/dist/src/cli/helpers/issue-tracker/github.js +203 -0
  25. package/dist/src/cli/helpers/issue-tracker/github.js.map +1 -1
  26. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  27. package/dist/src/cli/helpers/issue-tracker/index.js +127 -13
  28. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  29. package/dist/src/cli/helpers/issue-tracker/jira.d.ts +18 -2
  30. package/dist/src/cli/helpers/issue-tracker/jira.d.ts.map +1 -1
  31. package/dist/src/cli/helpers/issue-tracker/jira.js +331 -32
  32. package/dist/src/cli/helpers/issue-tracker/jira.js.map +1 -1
  33. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts.map +1 -1
  34. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js +61 -1
  35. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js.map +1 -1
  36. package/dist/src/cli/helpers/issue-tracker/types.d.ts +25 -0
  37. package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
  38. package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
  39. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +1 -1
  40. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  41. package/dist/src/utils/docs-preview/config-generator.js +12 -16
  42. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  43. package/dist/src/utils/docs-preview/docusaurus-setup.d.ts.map +1 -1
  44. package/dist/src/utils/docs-preview/docusaurus-setup.js +24 -5
  45. package/dist/src/utils/docs-preview/docusaurus-setup.js.map +1 -1
  46. package/dist/src/utils/docs-preview/package-installer.d.ts +3 -0
  47. package/dist/src/utils/docs-preview/package-installer.d.ts.map +1 -1
  48. package/dist/src/utils/docs-preview/package-installer.js +18 -9
  49. package/dist/src/utils/docs-preview/package-installer.js.map +1 -1
  50. package/dist/src/utils/docs-preview/server-manager.d.ts.map +1 -1
  51. package/dist/src/utils/docs-preview/server-manager.js +2 -1
  52. package/dist/src/utils/docs-preview/server-manager.js.map +1 -1
  53. package/dist/src/utils/validators/jira-validator.d.ts +37 -1
  54. package/dist/src/utils/validators/jira-validator.d.ts.map +1 -1
  55. package/dist/src/utils/validators/jira-validator.js +289 -183
  56. package/dist/src/utils/validators/jira-validator.js.map +1 -1
  57. package/package.json +3 -3
  58. package/plugins/specweave/commands/specweave-done.md +34 -0
  59. package/plugins/specweave/commands/specweave-increment.md +13 -5
  60. package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +3 -1
  61. package/plugins/specweave/hooks/docs-changed.sh.backup +0 -79
  62. package/plugins/specweave/hooks/human-input-required.sh.backup +0 -75
  63. package/plugins/specweave/hooks/post-first-increment.sh.backup +0 -61
  64. package/plugins/specweave/hooks/post-increment-change.sh.backup +0 -98
  65. package/plugins/specweave/hooks/post-increment-completion.sh.backup +0 -231
  66. package/plugins/specweave/hooks/post-increment-planning.sh.backup +0 -1048
  67. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +0 -147
  68. package/plugins/specweave/hooks/post-spec-update.sh.backup +0 -158
  69. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +0 -179
  70. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +0 -83
  71. package/plugins/specweave/hooks/pre-implementation.sh.backup +0 -67
  72. package/plugins/specweave/hooks/pre-task-completion.sh.backup +0 -194
  73. package/plugins/specweave/hooks/pre-tool-use.sh.backup +0 -133
  74. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +0 -386
  75. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +0 -353
  76. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +0 -172
  77. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -170
  78. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -1262
  79. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +0 -258
  80. package/plugins/specweave-github/lib/enhanced-github-sync.js +0 -220
  81. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +0 -172
  82. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +0 -134
  83. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1254
  84. 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
- // Load from .env
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
- this.domain = env.JIRA_DOMAIN || '';
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
- const url = `https://${this.domain}/rest/api/3/${endpoint}`;
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
- const env = this.loadEnv();
279
- const strategy = env.JIRA_STRATEGY || 'project-per-team';
280
- // Determine project key(s) based on strategy
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 (strategy === 'project-per-team') {
283
- // Multiple projects (JIRA_PROJECTS is comma-separated)
284
- const projectsEnv = env.JIRA_PROJECTS || '';
285
- if (!projectsEnv) {
286
- console.log(chalk.red('❌ JIRA_PROJECTS not found in .env'));
287
- result.valid = false;
288
- return result;
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
- // Single project (component-based or board-based)
294
- const projectKey = env.JIRA_PROJECT;
295
- if (!projectKey) {
296
- console.log(chalk.red('❌ JIRA_PROJECT not found in .env'));
297
- result.valid = false;
298
- return result;
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
- projectKeys = [projectKey];
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 .env (handle both single and multiple projects)
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 key in JIRA_PROJECTS
359
- const updatedKeys = projectKeys.map(k => k === projectKey ? selectedProject : k);
360
- await this.updateEnv({ JIRA_PROJECTS: updatedKeys.join(',') });
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
- await this.updateEnv({ JIRA_PROJECT: selectedProject });
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 .env with project IDs (for multi-project strategy)
427
- if (strategy === 'project-per-team' && allProjects.length > 0) {
428
- const projectIds = allProjects.map(p => p.id).join(',');
429
- await this.updateEnv({ JIRA_PROJECT_IDS: projectIds });
430
- result.envUpdated = true;
431
- console.log(chalk.green(`✅ Updated .env with project IDs: ${projectIds}\n`));
432
- }
433
- else if (allProjects.length === 1) {
434
- // Single project - store both key and ID
435
- await this.updateEnv({ JIRA_PROJECT_ID: allProjects[0].id });
436
- result.envUpdated = true;
437
- console.log(chalk.green(`✅ Updated .env with project ID: ${allProjects[0].id}\n`));
438
- }
439
- // 2. Validate boards (per-project OR legacy board-based strategy)
440
- result.boards = { valid: true, existing: [], missing: [], created: [] };
441
- // NEW: Check for per-project boards (JIRA_BOARDS_{ProjectKey})
442
- let hasPerProjectBoards = false;
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 (NEW!)
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 projectKey of projectKeys) {
456
- const perProjectKey = `JIRA_BOARDS_${projectKey}`;
457
- const boardsConfig = env[perProjectKey];
458
- if (boardsConfig) {
459
- const boardEntries = boardsConfig.split(',').map((b) => b.trim()).filter(b => b);
460
- if (boardEntries.length > 0) {
461
- console.log(chalk.gray(` Project: ${projectKey} (${boardEntries.length} boards)`));
462
- const finalBoardIds = [];
463
- for (const entry of boardEntries) {
464
- const isNumeric = /^\d+$/.test(entry);
465
- if (isNumeric) {
466
- // Entry is a board ID - validate it exists AND belongs to this project
467
- const boardId = parseInt(entry, 10);
468
- const board = await this.checkBoard(boardId);
469
- if (board) {
470
- // NEW: Validate board belongs to the correct project
471
- if (board.location?.projectKey && board.location.projectKey !== projectKey) {
472
- console.log(chalk.yellow(` ⚠️ Board ${boardId}: ${board.name} belongs to project ${board.location.projectKey}, not ${projectKey}`));
473
- console.log(chalk.gray(` Expected: ${projectKey}, Found: ${board.location.projectKey}`));
474
- result.boards.missing.push(entry);
475
- result.boards.valid = false;
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
- // Board exists and belongs to correct project (or project unknown - backward compat)
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
- else {
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
- // Entry is a board name - check for conflicts, then create it
497
- // NEW: Detect board name conflicts across projects
498
- if (boardNamesSeen.has(entry)) {
499
- const existingProject = boardNamesSeen.get(entry);
500
- console.log(chalk.yellow(` ⚠️ Board name conflict: "${entry}" already used in project ${existingProject}`));
501
- console.log(chalk.gray(` Tip: Use unique board names or append project suffix (e.g., "${entry}-${projectKey}")`));
502
- result.boards.missing.push(entry);
503
- result.boards.valid = false;
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
- else {
506
- console.log(chalk.blue(` 📦 Creating board: ${entry}...`));
507
- try {
508
- const board = await this.createBoard(entry, projectKey);
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
- // Update .env with final board IDs for this project
523
- if (finalBoardIds.length > 0) {
524
- await this.updateEnv({ [perProjectKey]: finalBoardIds.join(',') });
525
- result.envUpdated = true;
526
- console.log(chalk.green(` ✅ Updated ${perProjectKey}: ${finalBoardIds.join(',')}`));
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
- const boardsConfig = env.JIRA_BOARDS || '';
536
- if (boardsConfig && strategy === 'board-based') {
537
- console.log(chalk.gray(`Checking boards: ${boardsConfig}...`));
538
- // For board-based strategy, use the single project key
539
- const projectKeyForBoards = projectKeys[0];
540
- const boardEntries = boardsConfig.split(',').map((b) => b.trim());
541
- const finalBoardIds = [];
542
- for (const entry of boardEntries) {
543
- const isNumeric = /^\d+$/.test(entry);
544
- if (isNumeric) {
545
- // Entry is a board ID - validate it exists
546
- const boardId = parseInt(entry, 10);
547
- const board = await this.checkBoard(boardId);
548
- if (board) {
549
- console.log(chalk.green(` ✅ Board ${boardId}: ${board.name} (exists)`));
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
- // Entry is a board name - create it
561
- console.log(chalk.blue(` 📦 Creating board: ${entry}...`));
562
- try {
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
- console.log(chalk.green(`✅ All boards validated/created successfully\n`));
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
  }