specweave 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CLAUDE.md +100 -13
  2. package/README.md +97 -207
  3. package/bin/install-agents.sh +1 -1
  4. package/bin/install-commands.sh +1 -1
  5. package/bin/install-hooks.sh +1 -1
  6. package/bin/install-skills.sh +1 -1
  7. package/bin/specweave.js +32 -0
  8. package/dist/cli/commands/validate-jira.d.ts +35 -0
  9. package/dist/cli/commands/validate-jira.d.ts.map +1 -0
  10. package/dist/cli/commands/validate-jira.js +112 -0
  11. package/dist/cli/commands/validate-jira.js.map +1 -0
  12. package/dist/cli/commands/validate-plugins.d.ts +41 -0
  13. package/dist/cli/commands/validate-plugins.d.ts.map +1 -0
  14. package/dist/cli/commands/validate-plugins.js +171 -0
  15. package/dist/cli/commands/validate-plugins.js.map +1 -0
  16. package/dist/core/types/sync-profile.d.ts +177 -29
  17. package/dist/core/types/sync-profile.d.ts.map +1 -1
  18. package/dist/core/types/sync-profile.js +48 -1
  19. package/dist/core/types/sync-profile.js.map +1 -1
  20. package/dist/hooks/lib/translate-living-docs.d.ts.map +1 -1
  21. package/dist/hooks/lib/translate-living-docs.js +16 -7
  22. package/dist/hooks/lib/translate-living-docs.js.map +1 -1
  23. package/dist/metrics/dora-calculator.d.ts +7 -3
  24. package/dist/metrics/dora-calculator.d.ts.map +1 -1
  25. package/dist/metrics/dora-calculator.js +19 -6
  26. package/dist/metrics/dora-calculator.js.map +1 -1
  27. package/dist/metrics/report-generator.d.ts +17 -0
  28. package/dist/metrics/report-generator.d.ts.map +1 -0
  29. package/dist/metrics/report-generator.js +403 -0
  30. package/dist/metrics/report-generator.js.map +1 -0
  31. package/dist/utils/external-resource-validator.d.ts +102 -0
  32. package/dist/utils/external-resource-validator.d.ts.map +1 -0
  33. package/dist/utils/external-resource-validator.js +381 -0
  34. package/dist/utils/external-resource-validator.js.map +1 -0
  35. package/dist/utils/plugin-validator.d.ts +161 -0
  36. package/dist/utils/plugin-validator.d.ts.map +1 -0
  37. package/dist/utils/plugin-validator.js +565 -0
  38. package/dist/utils/plugin-validator.js.map +1 -0
  39. package/package.json +2 -1
  40. package/plugins/specweave/commands/specweave-do.md +47 -0
  41. package/plugins/specweave/commands/specweave-increment.md +82 -0
  42. package/plugins/specweave/commands/specweave-next.md +47 -0
  43. package/plugins/specweave/hooks/post-increment-planning.sh +117 -38
  44. package/plugins/specweave/hooks/pre-tool-use.sh +133 -0
  45. package/plugins/specweave/plugin.json +22 -0
  46. package/plugins/specweave/skills/SKILLS-INDEX.md +3 -1
  47. package/plugins/specweave/skills/plugin-validator/SKILL.md +427 -0
  48. package/plugins/specweave-ado/.claude-plugin/plugin.json +2 -4
  49. package/plugins/specweave-ado/lib/ado-board-resolver.ts +328 -0
  50. package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +484 -0
  51. package/plugins/specweave-ado/plugin.json +20 -0
  52. package/plugins/specweave-alternatives/.claude-plugin/plugin.json +15 -2
  53. package/plugins/specweave-backend/.claude-plugin/plugin.json +15 -2
  54. package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +14 -2
  55. package/plugins/specweave-diagrams/.claude-plugin/plugin.json +14 -2
  56. package/plugins/specweave-docs/.claude-plugin/plugin.json +13 -2
  57. package/plugins/specweave-figma/.claude-plugin/plugin.json +14 -2
  58. package/plugins/specweave-frontend/.claude-plugin/plugin.json +15 -2
  59. package/plugins/specweave-github/lib/github-board-resolver.ts +164 -0
  60. package/plugins/specweave-github/lib/github-hierarchical-sync.ts +344 -0
  61. package/plugins/specweave-github/plugin.json +19 -0
  62. package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +15 -2
  63. package/plugins/specweave-jira/.claude-plugin/plugin.json +14 -2
  64. package/plugins/specweave-jira/lib/jira-board-resolver.ts +127 -0
  65. package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +283 -0
  66. package/plugins/specweave-jira/plugin.json +20 -0
  67. package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +584 -0
  68. package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +14 -2
  69. package/plugins/specweave-payments/.claude-plugin/plugin.json +14 -2
  70. package/plugins/specweave-testing/.claude-plugin/plugin.json +14 -2
  71. package/plugins/specweave-tooling/.claude-plugin/plugin.json +13 -2
  72. package/src/templates/.env.example +71 -5
@@ -0,0 +1,164 @@
1
+ /**
2
+ * GitHub Project Board Resolution for Hierarchical Sync
3
+ *
4
+ * Resolves project board names to board IDs for use in search queries.
5
+ * Supports both GitHub Classic Projects (v1) and Projects v2 (beta).
6
+ */
7
+
8
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
9
+
10
+ /**
11
+ * GitHub Project Board (Classic Projects)
12
+ */
13
+ export interface GitHubProjectBoard {
14
+ id: number;
15
+ name: string;
16
+ number: number;
17
+ state: 'open' | 'closed';
18
+ html_url: string;
19
+ }
20
+
21
+ /**
22
+ * Fetch all project boards for a GitHub repository
23
+ *
24
+ * Uses GitHub CLI: gh api repos/{owner}/{repo}/projects
25
+ *
26
+ * @param owner Repository owner
27
+ * @param repo Repository name
28
+ * @returns Array of project boards
29
+ */
30
+ export async function fetchBoardsForRepo(
31
+ owner: string,
32
+ repo: string
33
+ ): Promise<GitHubProjectBoard[]> {
34
+ console.log(`🔍 Fetching project boards for repo: ${owner}/${repo}`);
35
+
36
+ try {
37
+ const result = await execFileNoThrow('gh', [
38
+ 'api',
39
+ `repos/${owner}/${repo}/projects`,
40
+ '--jq',
41
+ '.[] | {id: .id, name: .name, number: .number, state: .state, html_url: .html_url}',
42
+ '-H',
43
+ 'Accept: application/vnd.github+json',
44
+ ]);
45
+
46
+ if (result.status !== 0) {
47
+ console.error(`❌ Failed to fetch boards for ${owner}/${repo}:`, result.stderr);
48
+ throw new Error(`GitHub API error: ${result.status} ${result.stderr}`);
49
+ }
50
+
51
+ // Parse JSONL (one JSON object per line)
52
+ const boards: GitHubProjectBoard[] = result.stdout
53
+ .trim()
54
+ .split('\n')
55
+ .filter(line => line.trim())
56
+ .map(line => JSON.parse(line));
57
+
58
+ console.log(`✅ Found ${boards.length} board(s) for repo ${owner}/${repo}`);
59
+
60
+ return boards;
61
+ } catch (error) {
62
+ console.error(`❌ Error fetching boards for ${owner}/${repo}:`, (error as Error).message);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Fetch organization-level project boards
69
+ *
70
+ * Uses GitHub CLI: gh api orgs/{org}/projects
71
+ *
72
+ * @param org Organization name
73
+ * @returns Array of organization project boards
74
+ */
75
+ export async function fetchBoardsForOrg(
76
+ org: string
77
+ ): Promise<GitHubProjectBoard[]> {
78
+ console.log(`🔍 Fetching project boards for org: ${org}`);
79
+
80
+ try {
81
+ const result = await execFileNoThrow('gh', [
82
+ 'api',
83
+ `orgs/${org}/projects`,
84
+ '--jq',
85
+ '.[] | {id: .id, name: .name, number: .number, state: .state, html_url: .html_url}',
86
+ '-H',
87
+ 'Accept: application/vnd.github+json',
88
+ ]);
89
+
90
+ if (result.status !== 0) {
91
+ console.error(`❌ Failed to fetch boards for org ${org}:`, result.stderr);
92
+ throw new Error(`GitHub API error: ${result.status} ${result.stderr}`);
93
+ }
94
+
95
+ // Parse JSONL
96
+ const boards: GitHubProjectBoard[] = result.stdout
97
+ .trim()
98
+ .split('\n')
99
+ .filter(line => line.trim())
100
+ .map(line => JSON.parse(line));
101
+
102
+ console.log(`✅ Found ${boards.length} board(s) for org ${org}`);
103
+
104
+ return boards;
105
+ } catch (error) {
106
+ console.error(`❌ Error fetching boards for org ${org}:`, (error as Error).message);
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Resolve board names to board numbers (for repo-level projects)
113
+ *
114
+ * @param owner Repository owner
115
+ * @param repo Repository name
116
+ * @param boardNames Array of board names to resolve
117
+ * @returns Map of board name → board number
118
+ */
119
+ export async function resolveBoardNames(
120
+ owner: string,
121
+ repo: string,
122
+ boardNames: string[]
123
+ ): Promise<Map<string, number>> {
124
+ if (!boardNames || boardNames.length === 0) {
125
+ return new Map();
126
+ }
127
+
128
+ const boards = await fetchBoardsForRepo(owner, repo);
129
+
130
+ const boardMap = new Map<string, number>();
131
+
132
+ for (const boardName of boardNames) {
133
+ const board = boards.find(
134
+ (b) => b.name.toLowerCase() === boardName.toLowerCase()
135
+ );
136
+
137
+ if (board) {
138
+ boardMap.set(boardName, board.number);
139
+ console.log(`✅ Resolved board "${boardName}" → Number ${board.number}`);
140
+ } else {
141
+ console.warn(`⚠️ Board "${boardName}" not found in repo ${owner}/${repo}`);
142
+ // Don't throw - just skip this board (user may have typo or board was deleted)
143
+ }
144
+ }
145
+
146
+ return boardMap;
147
+ }
148
+
149
+ /**
150
+ * Get board numbers for a list of board names (helper function)
151
+ *
152
+ * @param owner Repository owner
153
+ * @param repo Repository name
154
+ * @param boardNames Array of board names
155
+ * @returns Array of board numbers (skips boards not found)
156
+ */
157
+ export async function getBoardNumbers(
158
+ owner: string,
159
+ repo: string,
160
+ boardNames: string[]
161
+ ): Promise<number[]> {
162
+ const boardMap = await resolveBoardNames(owner, repo, boardNames);
163
+ return Array.from(boardMap.values());
164
+ }
@@ -0,0 +1,344 @@
1
+ /**
2
+ * GitHub Hierarchical Sync Implementation
3
+ *
4
+ * Supports three sync strategies:
5
+ * 1. Simple: One repository, all issues (backward compatible)
6
+ * 2. Filtered: Multiple repositories + project boards + filters
7
+ * 3. Custom: Raw GitHub search query
8
+ */
9
+
10
+ import {
11
+ SyncProfile,
12
+ SyncContainer,
13
+ GitHubConfig,
14
+ isSimpleStrategy,
15
+ isFilteredStrategy,
16
+ isCustomStrategy,
17
+ TimeRangePreset,
18
+ } from '../../../src/core/types/sync-profile.js';
19
+ import { GitHubIssue } from './types.js';
20
+ import { getBoardNumbers } from './github-board-resolver.js';
21
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
22
+
23
+ /**
24
+ * Build hierarchical GitHub search query from containers
25
+ *
26
+ * Example output:
27
+ * repo:owner/repo-a repo:owner/repo-b is:issue label:feature milestone:"v2.0" created:2024-01-01..2024-12-31
28
+ *
29
+ * @param containers Array of containers (repos) with filters
30
+ * @returns GitHub search query string
31
+ */
32
+ export async function buildHierarchicalSearchQuery(
33
+ containers: SyncContainer[]
34
+ ): Promise<string> {
35
+ const parts: string[] = [];
36
+
37
+ // Add repo clauses
38
+ for (const container of containers) {
39
+ parts.push(`repo:${container.id}`);
40
+
41
+ // Note: GitHub search doesn't support filtering by project board directly
42
+ // Project boards would need to be handled via GraphQL API or issue filtering
43
+ if (container.subOrganizations && container.subOrganizations.length > 0) {
44
+ console.warn(
45
+ `⚠️ GitHub search doesn't support project board filtering directly.`
46
+ );
47
+ console.warn(
48
+ ` Boards will be ignored: ${container.subOrganizations.join(', ')}`
49
+ );
50
+ }
51
+ }
52
+
53
+ // Add is:issue filter
54
+ parts.push('is:issue');
55
+
56
+ // Add filters from first container (apply to all repos)
57
+ // Note: GitHub search applies filters globally, not per-repo
58
+ const filters = containers[0]?.filters;
59
+ if (filters) {
60
+ const filterClauses = buildFilterClauses(filters);
61
+ parts.push(...filterClauses);
62
+ }
63
+
64
+ return parts.join(' ');
65
+ }
66
+
67
+ /**
68
+ * Build filter clauses from container filters
69
+ *
70
+ * @param filters Container filters
71
+ * @returns Array of GitHub search filter clauses
72
+ */
73
+ function buildFilterClauses(filters: any): string[] {
74
+ const clauses: string[] = [];
75
+
76
+ // Include labels
77
+ if (filters.includeLabels && filters.includeLabels.length > 0) {
78
+ for (const label of filters.includeLabels) {
79
+ clauses.push(`label:"${label}"`);
80
+ }
81
+ }
82
+
83
+ // Exclude labels (GitHub uses -label:)
84
+ if (filters.excludeLabels && filters.excludeLabels.length > 0) {
85
+ for (const label of filters.excludeLabels) {
86
+ clauses.push(`-label:"${label}"`);
87
+ }
88
+ }
89
+
90
+ // Assignees
91
+ if (filters.assignees && filters.assignees.length > 0) {
92
+ // GitHub supports multiple assignees with OR logic
93
+ const assigneeQuery = filters.assignees
94
+ .map((a: string) => `assignee:"${a}"`)
95
+ .join(' ');
96
+ clauses.push(assigneeQuery);
97
+ }
98
+
99
+ // Status (GitHub uses is:open, is:closed)
100
+ if (filters.statusCategories && filters.statusCategories.length > 0) {
101
+ const statuses = filters.statusCategories.map((s: string) => s.toLowerCase());
102
+ if (statuses.includes('open') || statuses.includes('to do') || statuses.includes('in progress')) {
103
+ clauses.push('is:open');
104
+ } else if (statuses.includes('closed') || statuses.includes('done')) {
105
+ clauses.push('is:closed');
106
+ }
107
+ }
108
+
109
+ // Milestones (GitHub-specific)
110
+ if (filters.milestones && filters.milestones.length > 0) {
111
+ for (const milestone of filters.milestones) {
112
+ clauses.push(`milestone:"${milestone}"`);
113
+ }
114
+ }
115
+
116
+ return clauses;
117
+ }
118
+
119
+ /**
120
+ * Add time range filter to GitHub search query
121
+ *
122
+ * @param query Base search query
123
+ * @param timeRange Time range preset (1W, 1M, 3M, 6M, ALL)
124
+ * @returns Search query with time range filter
125
+ */
126
+ function addTimeRangeFilter(query: string, timeRange: string): string {
127
+ if (timeRange === 'ALL') {
128
+ return query; // No time filter
129
+ }
130
+
131
+ const { since, until } = calculateTimeRange(timeRange as TimeRangePreset);
132
+
133
+ return `${query} created:${since}..${until}`;
134
+ }
135
+
136
+ /**
137
+ * Calculate date range from time range preset
138
+ */
139
+ function calculateTimeRange(timeRange: TimeRangePreset): {
140
+ since: string;
141
+ until: string;
142
+ } {
143
+ const now = new Date();
144
+ const since = new Date(now);
145
+
146
+ switch (timeRange) {
147
+ case '1W':
148
+ since.setDate(now.getDate() - 7);
149
+ break;
150
+ case '2W':
151
+ since.setDate(now.getDate() - 14);
152
+ break;
153
+ case '1M':
154
+ since.setMonth(now.getMonth() - 1);
155
+ break;
156
+ case '3M':
157
+ since.setMonth(now.getMonth() - 3);
158
+ break;
159
+ case '6M':
160
+ since.setMonth(now.getMonth() - 6);
161
+ break;
162
+ case '1Y':
163
+ since.setFullYear(now.getFullYear() - 1);
164
+ break;
165
+ case 'ALL':
166
+ return {
167
+ since: '1970-01-01',
168
+ until: now.toISOString().split('T')[0],
169
+ };
170
+ }
171
+
172
+ return {
173
+ since: since.toISOString().split('T')[0],
174
+ until: now.toISOString().split('T')[0],
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Fetch issues hierarchically based on sync strategy
180
+ *
181
+ * @param profile Sync profile with strategy
182
+ * @param timeRange Time range preset
183
+ * @returns Array of GitHub issues
184
+ */
185
+ export async function fetchIssuesHierarchical(
186
+ profile: SyncProfile,
187
+ timeRange: string = '1M'
188
+ ): Promise<GitHubIssue[]> {
189
+ const config = profile.config as GitHubConfig;
190
+
191
+ // Strategy 1: SIMPLE (backward compatible)
192
+ if (isSimpleStrategy(profile)) {
193
+ return fetchIssuesSimple(config, timeRange);
194
+ }
195
+
196
+ // Strategy 2: CUSTOM (raw search query)
197
+ if (isCustomStrategy(profile)) {
198
+ return fetchIssuesCustom(config, timeRange);
199
+ }
200
+
201
+ // Strategy 3: FILTERED (hierarchical)
202
+ if (isFilteredStrategy(profile)) {
203
+ return fetchIssuesFiltered(config, timeRange);
204
+ }
205
+
206
+ // Default to simple if strategy not recognized
207
+ console.warn('⚠️ Unknown strategy, defaulting to simple');
208
+ return fetchIssuesSimple(config, timeRange);
209
+ }
210
+
211
+ /**
212
+ * Fetch issues using SIMPLE strategy (one repo, all issues)
213
+ *
214
+ * @param config GitHub configuration
215
+ * @param timeRange Time range preset
216
+ * @returns Array of GitHub issues
217
+ */
218
+ async function fetchIssuesSimple(
219
+ config: GitHubConfig,
220
+ timeRange: string
221
+ ): Promise<GitHubIssue[]> {
222
+ const owner = config.owner;
223
+ const repo = config.repo;
224
+
225
+ if (!owner || !repo) {
226
+ throw new Error('Simple strategy requires owner and repo in config');
227
+ }
228
+
229
+ let query = `repo:${owner}/${repo} is:issue`;
230
+
231
+ // Add time range
232
+ query = addTimeRangeFilter(query, timeRange);
233
+
234
+ console.log('🔍 Fetching issues (SIMPLE strategy):', query);
235
+
236
+ return executeSearch(query);
237
+ }
238
+
239
+ /**
240
+ * Fetch issues using CUSTOM strategy (raw search query)
241
+ *
242
+ * @param config GitHub configuration
243
+ * @param timeRange Time range preset
244
+ * @returns Array of GitHub issues
245
+ */
246
+ async function fetchIssuesCustom(
247
+ config: GitHubConfig,
248
+ timeRange: string
249
+ ): Promise<GitHubIssue[]> {
250
+ const customQuery = config.customQuery;
251
+
252
+ if (!customQuery) {
253
+ throw new Error('Custom strategy requires customQuery in config');
254
+ }
255
+
256
+ // Add time range to custom query
257
+ const query = addTimeRangeFilter(customQuery, timeRange);
258
+
259
+ console.log('🔍 Fetching issues (CUSTOM strategy):', query);
260
+
261
+ return executeSearch(query);
262
+ }
263
+
264
+ /**
265
+ * Fetch issues using FILTERED strategy (multiple repos + filters)
266
+ *
267
+ * @param config GitHub configuration
268
+ * @param timeRange Time range preset
269
+ * @returns Array of GitHub issues
270
+ */
271
+ async function fetchIssuesFiltered(
272
+ config: GitHubConfig,
273
+ timeRange: string
274
+ ): Promise<GitHubIssue[]> {
275
+ const containers = config.containers;
276
+
277
+ if (!containers || containers.length === 0) {
278
+ throw new Error('Filtered strategy requires containers array in config');
279
+ }
280
+
281
+ // Build hierarchical search query
282
+ const baseQuery = await buildHierarchicalSearchQuery(containers);
283
+
284
+ // Add time range
285
+ const query = addTimeRangeFilter(baseQuery, timeRange);
286
+
287
+ console.log('🔍 Fetching issues (FILTERED strategy):', query);
288
+
289
+ return executeSearch(query);
290
+ }
291
+
292
+ /**
293
+ * Execute GitHub search and return issues
294
+ *
295
+ * @param query GitHub search query
296
+ * @returns Array of GitHub issues
297
+ */
298
+ async function executeSearch(query: string): Promise<GitHubIssue[]> {
299
+ const result = await execFileNoThrow('gh', [
300
+ 'search',
301
+ 'issues',
302
+ query,
303
+ '--json',
304
+ 'number,title,body,state,url,labels,milestone,repository',
305
+ '--limit',
306
+ '1000', // Max results
307
+ ]);
308
+
309
+ if (result.status !== 0) {
310
+ throw new Error(`Failed to search issues: ${result.stderr || result.stdout}`);
311
+ }
312
+
313
+ if (!result.stdout.trim()) {
314
+ return [];
315
+ }
316
+
317
+ const issues = JSON.parse(result.stdout);
318
+
319
+ // Transform to GitHubIssue format
320
+ return issues.map((issue: any) => ({
321
+ number: issue.number,
322
+ title: issue.title,
323
+ body: issue.body || '',
324
+ state: issue.state,
325
+ html_url: issue.url,
326
+ url: issue.url,
327
+ labels: issue.labels?.map((l: any) => l.name) || [],
328
+ milestone: issue.milestone
329
+ ? {
330
+ number: issue.milestone.number,
331
+ title: issue.milestone.title,
332
+ description: issue.milestone.description,
333
+ state: issue.milestone.state,
334
+ }
335
+ : undefined,
336
+ repository: issue.repository
337
+ ? {
338
+ owner: issue.repository.owner.login,
339
+ name: issue.repository.name,
340
+ full_name: issue.repository.full_name,
341
+ }
342
+ : undefined,
343
+ }));
344
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "specweave-github",
3
+ "description": "GitHub Issues integration for SpecWeave increments. Bidirectional sync between SpecWeave increments and GitHub Issues. Automatically creates issues from increments, tracks progress, and closes issues on completion. Uses GitHub CLI (gh) for seamless integration.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "SpecWeave Team",
7
+ "url": "https://spec-weave.com"
8
+ },
9
+ "homepage": "https://spec-weave.com",
10
+ "repository": "https://github.com/anton-abyzov/specweave",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "github",
14
+ "issues",
15
+ "integration",
16
+ "sync",
17
+ "specweave"
18
+ ]
19
+ }
@@ -3,6 +3,19 @@
3
3
  "description": "Cloud infrastructure provisioning and monitoring. Includes Hetzner Cloud provisioning, Prometheus/Grafana setup, distributed tracing (Jaeger/Tempo), and SLO implementation. Focus on cost-effective, production-ready infrastructure.",
4
4
  "version": "1.0.0",
5
5
  "author": {
6
- "name": "SpecWeave Team"
7
- }
6
+ "name": "SpecWeave Team",
7
+ "url": "https://spec-weave.com"
8
+ },
9
+ "homepage": "https://spec-weave.com",
10
+ "repository": "https://github.com/anton-abyzov/specweave",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "infrastructure",
14
+ "cloud",
15
+ "hetzner",
16
+ "monitoring",
17
+ "prometheus",
18
+ "grafana",
19
+ "specweave"
20
+ ]
8
21
  }
@@ -3,6 +3,18 @@
3
3
  "description": "JIRA integration for SpecWeave increments. Bidirectional sync between SpecWeave increments and JIRA epics/stories. Automatically creates JIRA issues from increments, tracks progress, and updates status.",
4
4
  "version": "1.0.0",
5
5
  "author": {
6
- "name": "SpecWeave Team"
7
- }
6
+ "name": "SpecWeave Team",
7
+ "url": "https://spec-weave.com"
8
+ },
9
+ "homepage": "https://spec-weave.com",
10
+ "repository": "https://github.com/anton-abyzov/specweave",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "jira",
14
+ "atlassian",
15
+ "integration",
16
+ "sync",
17
+ "specweave",
18
+ "project-management"
19
+ ]
8
20
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Jira Board Resolution for Hierarchical Sync
3
+ *
4
+ * Resolves board names to board IDs for use in JQL queries.
5
+ * Supports Jira Agile (Software) boards.
6
+ */
7
+
8
+ import { JiraClient } from '../../../src/integrations/jira/jira-client.js';
9
+
10
+ /**
11
+ * Jira Board (Agile API)
12
+ */
13
+ export interface JiraBoard {
14
+ id: number;
15
+ name: string;
16
+ type: 'scrum' | 'kanban' | 'simple';
17
+ self: string;
18
+ location?: {
19
+ projectId: number;
20
+ projectKey: string;
21
+ projectName: string;
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Fetch all boards for a Jira project
27
+ *
28
+ * Uses Jira Agile REST API: GET /rest/agile/1.0/board?projectKeyOrId={projectKey}
29
+ *
30
+ * @param client JiraClient instance
31
+ * @param projectKey Jira project key (e.g., "PROJECT-A")
32
+ * @returns Array of boards in the project
33
+ */
34
+ export async function fetchBoardsForProject(
35
+ client: JiraClient,
36
+ projectKey: string
37
+ ): Promise<JiraBoard[]> {
38
+ console.log(`🔍 Fetching boards for project: ${projectKey}`);
39
+
40
+ try {
41
+ // Access private baseUrl and getAuthHeader via reflection (not ideal but necessary)
42
+ const baseUrl = (client as any).baseUrl;
43
+ const authHeader = (client as any).getAuthHeader();
44
+
45
+ const url = `${baseUrl}/rest/agile/1.0/board?projectKeyOrId=${projectKey}`;
46
+
47
+ const response = await fetch(url, {
48
+ method: 'GET',
49
+ headers: {
50
+ 'Authorization': authHeader,
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ });
54
+
55
+ if (!response.ok) {
56
+ const error = await response.text();
57
+ console.error(`❌ Failed to fetch boards for ${projectKey}:`, error);
58
+ throw new Error(`Jira API error: ${response.status} ${error}`);
59
+ }
60
+
61
+ const data = await response.json();
62
+
63
+ const boards: JiraBoard[] = data.values || [];
64
+
65
+ console.log(`✅ Found ${boards.length} board(s) for project ${projectKey}`);
66
+
67
+ return boards;
68
+ } catch (error) {
69
+ console.error(`❌ Error fetching boards for ${projectKey}:`, (error as Error).message);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Resolve board names to board IDs
76
+ *
77
+ * @param client JiraClient instance
78
+ * @param projectKey Jira project key
79
+ * @param boardNames Array of board names to resolve
80
+ * @returns Map of board name → board ID
81
+ */
82
+ export async function resolveBoardNames(
83
+ client: JiraClient,
84
+ projectKey: string,
85
+ boardNames: string[]
86
+ ): Promise<Map<string, number>> {
87
+ if (!boardNames || boardNames.length === 0) {
88
+ return new Map();
89
+ }
90
+
91
+ const boards = await fetchBoardsForProject(client, projectKey);
92
+
93
+ const boardMap = new Map<string, number>();
94
+
95
+ for (const boardName of boardNames) {
96
+ const board = boards.find(
97
+ (b) => b.name.toLowerCase() === boardName.toLowerCase()
98
+ );
99
+
100
+ if (board) {
101
+ boardMap.set(boardName, board.id);
102
+ console.log(`✅ Resolved board "${boardName}" → ID ${board.id}`);
103
+ } else {
104
+ console.warn(`⚠️ Board "${boardName}" not found in project ${projectKey}`);
105
+ // Don't throw - just skip this board (user may have typo or board was deleted)
106
+ }
107
+ }
108
+
109
+ return boardMap;
110
+ }
111
+
112
+ /**
113
+ * Get board IDs for a list of board names (helper function)
114
+ *
115
+ * @param client JiraClient instance
116
+ * @param projectKey Jira project key
117
+ * @param boardNames Array of board names
118
+ * @returns Array of board IDs (skips boards not found)
119
+ */
120
+ export async function getBoardIds(
121
+ client: JiraClient,
122
+ projectKey: string,
123
+ boardNames: string[]
124
+ ): Promise<number[]> {
125
+ const boardMap = await resolveBoardNames(client, projectKey, boardNames);
126
+ return Array.from(boardMap.values());
127
+ }