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.
- package/CLAUDE.md +100 -13
- package/README.md +97 -207
- 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/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 +381 -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 +3 -1
- 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 +584 -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
- package/src/templates/.env.example +71 -5
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira Hierarchical Sync Implementation
|
|
3
|
+
*
|
|
4
|
+
* Supports three sync strategies:
|
|
5
|
+
* 1. Simple: One project, all boards (backward compatible)
|
|
6
|
+
* 2. Filtered: Multiple projects + specific boards + filters
|
|
7
|
+
* 3. Custom: Raw JQL query
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
SyncProfile,
|
|
12
|
+
SyncContainer,
|
|
13
|
+
JiraConfig,
|
|
14
|
+
isSimpleStrategy,
|
|
15
|
+
isFilteredStrategy,
|
|
16
|
+
isCustomStrategy,
|
|
17
|
+
} from '../../../src/core/types/sync-profile.js';
|
|
18
|
+
import { JiraClient, JiraIssue } from '../../../src/integrations/jira/jira-client.js';
|
|
19
|
+
import { getBoardIds } from './jira-board-resolver.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build hierarchical JQL query from containers
|
|
23
|
+
*
|
|
24
|
+
* Example output:
|
|
25
|
+
* (project=PROJECT-A AND board IN (123, 456) AND labels IN (feature))
|
|
26
|
+
* OR
|
|
27
|
+
* (project=PROJECT-B AND board IN (789))
|
|
28
|
+
*
|
|
29
|
+
* @param client JiraClient instance
|
|
30
|
+
* @param containers Array of containers with projects + boards + filters
|
|
31
|
+
* @returns JQL query string
|
|
32
|
+
*/
|
|
33
|
+
export async function buildHierarchicalJQL(
|
|
34
|
+
client: JiraClient,
|
|
35
|
+
containers: SyncContainer[]
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const clauses: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const container of containers) {
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
|
|
42
|
+
// Project clause
|
|
43
|
+
parts.push(`project=${container.id}`);
|
|
44
|
+
|
|
45
|
+
// Board filtering (if sub-organizations specified)
|
|
46
|
+
if (container.subOrganizations && container.subOrganizations.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
const boardIds = await getBoardIds(
|
|
49
|
+
client,
|
|
50
|
+
container.id,
|
|
51
|
+
container.subOrganizations
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (boardIds.length > 0) {
|
|
55
|
+
parts.push(`board IN (${boardIds.join(',')})`);
|
|
56
|
+
} else {
|
|
57
|
+
console.warn(
|
|
58
|
+
`⚠️ No valid boards found for project ${container.id}, syncing all boards`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`⚠️ Failed to resolve boards for ${container.id}, syncing all boards:`,
|
|
64
|
+
(error as Error).message
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Filters
|
|
70
|
+
if (container.filters) {
|
|
71
|
+
const filterClauses = buildFilterClauses(container.filters);
|
|
72
|
+
parts.push(...filterClauses);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Combine parts with AND
|
|
76
|
+
clauses.push(`(${parts.join(' AND ')})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Combine clauses with OR
|
|
80
|
+
return clauses.join(' OR ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build filter clauses from container filters
|
|
85
|
+
*
|
|
86
|
+
* @param filters Container filters
|
|
87
|
+
* @returns Array of JQL filter clauses
|
|
88
|
+
*/
|
|
89
|
+
function buildFilterClauses(filters: any): string[] {
|
|
90
|
+
const clauses: string[] = [];
|
|
91
|
+
|
|
92
|
+
// Include labels
|
|
93
|
+
if (filters.includeLabels && filters.includeLabels.length > 0) {
|
|
94
|
+
const labels = filters.includeLabels.map((l: string) => `"${l}"`).join(', ');
|
|
95
|
+
clauses.push(`labels IN (${labels})`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Exclude labels
|
|
99
|
+
if (filters.excludeLabels && filters.excludeLabels.length > 0) {
|
|
100
|
+
const labels = filters.excludeLabels.map((l: string) => `"${l}"`).join(', ');
|
|
101
|
+
clauses.push(`labels NOT IN (${labels})`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Assignees
|
|
105
|
+
if (filters.assignees && filters.assignees.length > 0) {
|
|
106
|
+
const assignees = filters.assignees.map((a: string) => `"${a}"`).join(', ');
|
|
107
|
+
clauses.push(`assignee IN (${assignees})`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Status categories
|
|
111
|
+
if (filters.statusCategories && filters.statusCategories.length > 0) {
|
|
112
|
+
const statuses = filters.statusCategories.map((s: string) => `"${s}"`).join(', ');
|
|
113
|
+
clauses.push(`status IN (${statuses})`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Components (Jira-specific)
|
|
117
|
+
if (filters.components && filters.components.length > 0) {
|
|
118
|
+
const components = filters.components.map((c: string) => `"${c}"`).join(', ');
|
|
119
|
+
clauses.push(`component IN (${components})`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Sprints (Jira-specific)
|
|
123
|
+
if (filters.sprints && filters.sprints.length > 0) {
|
|
124
|
+
const sprints = filters.sprints.map((s: string) => `"${s}"`).join(', ');
|
|
125
|
+
clauses.push(`sprint IN (${sprints})`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Issue types (Jira-specific)
|
|
129
|
+
if (filters.issueTypes && filters.issueTypes.length > 0) {
|
|
130
|
+
const types = filters.issueTypes.map((t: string) => `"${t}"`).join(', ');
|
|
131
|
+
clauses.push(`issuetype IN (${types})`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return clauses;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Add time range filter to JQL query
|
|
139
|
+
*
|
|
140
|
+
* @param jql Base JQL query
|
|
141
|
+
* @param timeRange Time range preset (1W, 1M, 3M, 6M, ALL)
|
|
142
|
+
* @returns JQL with time range filter
|
|
143
|
+
*/
|
|
144
|
+
function addTimeRangeFilter(jql: string, timeRange: string): string {
|
|
145
|
+
if (timeRange === 'ALL') {
|
|
146
|
+
return jql; // No time filter
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Convert time range to Jira format
|
|
150
|
+
const timeMap: Record<string, string> = {
|
|
151
|
+
'1W': '-1w',
|
|
152
|
+
'2W': '-2w',
|
|
153
|
+
'1M': '-1M',
|
|
154
|
+
'3M': '-3M',
|
|
155
|
+
'6M': '-6M',
|
|
156
|
+
'1Y': '-1y',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const jiraTime = timeMap[timeRange] || '-1M';
|
|
160
|
+
|
|
161
|
+
return `${jql} AND created >= ${jiraTime}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fetch issues hierarchically based on sync strategy
|
|
166
|
+
*
|
|
167
|
+
* @param client JiraClient instance
|
|
168
|
+
* @param profile Sync profile with strategy
|
|
169
|
+
* @param timeRange Time range preset
|
|
170
|
+
* @returns Array of Jira issues
|
|
171
|
+
*/
|
|
172
|
+
export async function fetchIssuesHierarchical(
|
|
173
|
+
client: JiraClient,
|
|
174
|
+
profile: SyncProfile,
|
|
175
|
+
timeRange: string = '1M'
|
|
176
|
+
): Promise<JiraIssue[]> {
|
|
177
|
+
const config = profile.config as JiraConfig;
|
|
178
|
+
|
|
179
|
+
// Strategy 1: SIMPLE (backward compatible)
|
|
180
|
+
if (isSimpleStrategy(profile)) {
|
|
181
|
+
return fetchIssuesSimple(client, config, timeRange);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Strategy 2: CUSTOM (raw JQL)
|
|
185
|
+
if (isCustomStrategy(profile)) {
|
|
186
|
+
return fetchIssuesCustom(client, config, timeRange);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Strategy 3: FILTERED (hierarchical)
|
|
190
|
+
if (isFilteredStrategy(profile)) {
|
|
191
|
+
return fetchIssuesFiltered(client, config, timeRange);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Default to simple if strategy not recognized
|
|
195
|
+
console.warn('⚠️ Unknown strategy, defaulting to simple');
|
|
196
|
+
return fetchIssuesSimple(client, config, timeRange);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Fetch issues using SIMPLE strategy (one project, all boards)
|
|
201
|
+
*
|
|
202
|
+
* @param client JiraClient instance
|
|
203
|
+
* @param config Jira configuration
|
|
204
|
+
* @param timeRange Time range preset
|
|
205
|
+
* @returns Array of Jira issues
|
|
206
|
+
*/
|
|
207
|
+
async function fetchIssuesSimple(
|
|
208
|
+
client: JiraClient,
|
|
209
|
+
config: JiraConfig,
|
|
210
|
+
timeRange: string
|
|
211
|
+
): Promise<JiraIssue[]> {
|
|
212
|
+
const projectKey = config.projectKey;
|
|
213
|
+
|
|
214
|
+
if (!projectKey) {
|
|
215
|
+
throw new Error('Simple strategy requires projectKey in config');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let jql = `project=${projectKey}`;
|
|
219
|
+
|
|
220
|
+
// Add time range
|
|
221
|
+
jql = addTimeRangeFilter(jql, timeRange);
|
|
222
|
+
|
|
223
|
+
console.log('🔍 Fetching issues (SIMPLE strategy):', jql);
|
|
224
|
+
|
|
225
|
+
return client.searchIssues({ jql });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Fetch issues using CUSTOM strategy (raw JQL)
|
|
230
|
+
*
|
|
231
|
+
* @param client JiraClient instance
|
|
232
|
+
* @param config Jira configuration
|
|
233
|
+
* @param timeRange Time range preset
|
|
234
|
+
* @returns Array of Jira issues
|
|
235
|
+
*/
|
|
236
|
+
async function fetchIssuesCustom(
|
|
237
|
+
client: JiraClient,
|
|
238
|
+
config: JiraConfig,
|
|
239
|
+
timeRange: string
|
|
240
|
+
): Promise<JiraIssue[]> {
|
|
241
|
+
const customQuery = config.customQuery;
|
|
242
|
+
|
|
243
|
+
if (!customQuery) {
|
|
244
|
+
throw new Error('Custom strategy requires customQuery in config');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add time range to custom query
|
|
248
|
+
const jql = addTimeRangeFilter(customQuery, timeRange);
|
|
249
|
+
|
|
250
|
+
console.log('🔍 Fetching issues (CUSTOM strategy):', jql);
|
|
251
|
+
|
|
252
|
+
return client.searchIssues({ jql });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Fetch issues using FILTERED strategy (multiple projects + boards + filters)
|
|
257
|
+
*
|
|
258
|
+
* @param client JiraClient instance
|
|
259
|
+
* @param config Jira configuration
|
|
260
|
+
* @param timeRange Time range preset
|
|
261
|
+
* @returns Array of Jira issues
|
|
262
|
+
*/
|
|
263
|
+
async function fetchIssuesFiltered(
|
|
264
|
+
client: JiraClient,
|
|
265
|
+
config: JiraConfig,
|
|
266
|
+
timeRange: string
|
|
267
|
+
): Promise<JiraIssue[]> {
|
|
268
|
+
const containers = config.containers;
|
|
269
|
+
|
|
270
|
+
if (!containers || containers.length === 0) {
|
|
271
|
+
throw new Error('Filtered strategy requires containers array in config');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Build hierarchical JQL
|
|
275
|
+
const baseJql = await buildHierarchicalJQL(client, containers);
|
|
276
|
+
|
|
277
|
+
// Add time range
|
|
278
|
+
const jql = addTimeRangeFilter(baseJql, timeRange);
|
|
279
|
+
|
|
280
|
+
console.log('🔍 Fetching issues (FILTERED strategy):', jql);
|
|
281
|
+
|
|
282
|
+
return client.searchIssues({ jql });
|
|
283
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specweave-jira",
|
|
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
|
+
"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
|
+
"jira",
|
|
14
|
+
"atlassian",
|
|
15
|
+
"integration",
|
|
16
|
+
"sync",
|
|
17
|
+
"specweave",
|
|
18
|
+
"project-management"
|
|
19
|
+
]
|
|
20
|
+
}
|