specweave 0.9.1 → 0.10.1
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 +153 -13
- package/README.md +97 -251
- package/bin/install-agents.sh +1 -1
- package/bin/install-commands.sh +1 -1
- package/bin/install-hooks.sh +1 -1
- package/bin/install-skills.sh +1 -1
- package/bin/specweave.js +32 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +29 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/validate-jira.d.ts +35 -0
- package/dist/cli/commands/validate-jira.d.ts.map +1 -0
- package/dist/cli/commands/validate-jira.js +112 -0
- package/dist/cli/commands/validate-jira.js.map +1 -0
- package/dist/cli/commands/validate-plugins.d.ts +41 -0
- package/dist/cli/commands/validate-plugins.d.ts.map +1 -0
- package/dist/cli/commands/validate-plugins.js +171 -0
- package/dist/cli/commands/validate-plugins.js.map +1 -0
- package/dist/core/types/sync-profile.d.ts +177 -29
- package/dist/core/types/sync-profile.d.ts.map +1 -1
- package/dist/core/types/sync-profile.js +48 -1
- package/dist/core/types/sync-profile.js.map +1 -1
- package/dist/hooks/lib/translate-living-docs.d.ts.map +1 -1
- package/dist/hooks/lib/translate-living-docs.js +16 -7
- package/dist/hooks/lib/translate-living-docs.js.map +1 -1
- package/dist/metrics/dora-calculator.d.ts +7 -3
- package/dist/metrics/dora-calculator.d.ts.map +1 -1
- package/dist/metrics/dora-calculator.js +19 -6
- package/dist/metrics/dora-calculator.js.map +1 -1
- package/dist/metrics/report-generator.d.ts +17 -0
- package/dist/metrics/report-generator.d.ts.map +1 -0
- package/dist/metrics/report-generator.js +403 -0
- package/dist/metrics/report-generator.js.map +1 -0
- package/dist/utils/external-resource-validator.d.ts +102 -0
- package/dist/utils/external-resource-validator.d.ts.map +1 -0
- package/dist/utils/external-resource-validator.js +400 -0
- package/dist/utils/external-resource-validator.js.map +1 -0
- package/dist/utils/plugin-validator.d.ts +161 -0
- package/dist/utils/plugin-validator.d.ts.map +1 -0
- package/dist/utils/plugin-validator.js +565 -0
- package/dist/utils/plugin-validator.js.map +1 -0
- package/package.json +2 -1
- package/plugins/specweave/commands/specweave-do.md +47 -0
- package/plugins/specweave/commands/specweave-increment.md +82 -0
- package/plugins/specweave/commands/specweave-next.md +47 -0
- package/plugins/specweave/hooks/post-increment-planning.sh +117 -38
- package/plugins/specweave/hooks/pre-tool-use.sh +133 -0
- package/plugins/specweave/plugin.json +22 -0
- package/plugins/specweave/skills/SKILLS-INDEX.md +23 -2
- package/plugins/specweave/skills/plugin-installer/SKILL.md +340 -0
- package/plugins/specweave/skills/plugin-validator/SKILL.md +427 -0
- package/plugins/specweave-ado/.claude-plugin/plugin.json +2 -4
- package/plugins/specweave-ado/lib/ado-board-resolver.ts +328 -0
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +484 -0
- package/plugins/specweave-ado/plugin.json +20 -0
- package/plugins/specweave-alternatives/.claude-plugin/plugin.json +15 -2
- package/plugins/specweave-backend/.claude-plugin/plugin.json +15 -2
- package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-diagrams/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-docs/.claude-plugin/plugin.json +13 -2
- package/plugins/specweave-figma/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-frontend/.claude-plugin/plugin.json +15 -2
- package/plugins/specweave-github/lib/github-board-resolver.ts +164 -0
- package/plugins/specweave-github/lib/github-hierarchical-sync.ts +344 -0
- package/plugins/specweave-github/plugin.json +19 -0
- package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +15 -2
- package/plugins/specweave-jira/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-jira/lib/jira-board-resolver.ts +127 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +283 -0
- package/plugins/specweave-jira/plugin.json +20 -0
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +647 -0
- package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-payments/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-testing/.claude-plugin/plugin.json +14 -2
- package/plugins/specweave-tooling/.claude-plugin/plugin.json +13 -2
|
@@ -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
|
+
}
|