specweave 0.7.1 → 0.8.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 +307 -11
- package/README.md +41 -3
- package/dist/cli/commands/import-docs.d.ts +21 -0
- package/dist/cli/commands/import-docs.d.ts.map +1 -0
- package/dist/cli/commands/import-docs.js +146 -0
- package/dist/cli/commands/import-docs.js.map +1 -0
- package/dist/cli/commands/init-multiproject.d.ts +11 -0
- package/dist/cli/commands/init-multiproject.d.ts.map +1 -0
- package/dist/cli/commands/init-multiproject.js +202 -0
- package/dist/cli/commands/init-multiproject.js.map +1 -0
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrate-to-multiproject.d.ts +37 -0
- package/dist/cli/commands/migrate-to-multiproject.d.ts.map +1 -0
- package/dist/cli/commands/migrate-to-multiproject.js +189 -0
- package/dist/cli/commands/migrate-to-multiproject.js.map +1 -0
- package/dist/cli/commands/migrate-to-profiles.d.ts +25 -0
- package/dist/cli/commands/migrate-to-profiles.d.ts.map +1 -0
- package/dist/cli/commands/migrate-to-profiles.js +350 -0
- package/dist/cli/commands/migrate-to-profiles.js.map +1 -0
- package/dist/cli/commands/switch-project.d.ts +13 -0
- package/dist/cli/commands/switch-project.d.ts.map +1 -0
- package/dist/cli/commands/switch-project.js +91 -0
- package/dist/cli/commands/switch-project.js.map +1 -0
- package/dist/cli/helpers/issue-tracker/index.js +4 -4
- package/dist/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/cli/helpers/issue-tracker/utils.d.ts +6 -3
- package/dist/cli/helpers/issue-tracker/utils.d.ts.map +1 -1
- package/dist/cli/helpers/issue-tracker/utils.js +9 -7
- package/dist/cli/helpers/issue-tracker/utils.js.map +1 -1
- package/dist/core/brownfield/analyzer.d.ts +86 -0
- package/dist/core/brownfield/analyzer.d.ts.map +1 -0
- package/dist/core/brownfield/analyzer.js +365 -0
- package/dist/core/brownfield/analyzer.js.map +1 -0
- package/dist/core/brownfield/importer.d.ts +76 -0
- package/dist/core/brownfield/importer.d.ts.map +1 -0
- package/dist/core/brownfield/importer.js +287 -0
- package/dist/core/brownfield/importer.js.map +1 -0
- package/dist/core/config-manager.d.ts +47 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +136 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/project-manager.d.ts +127 -0
- package/dist/core/project-manager.d.ts.map +1 -0
- package/dist/core/project-manager.js +524 -0
- package/dist/core/project-manager.js.map +1 -0
- package/dist/core/sync/profile-manager.d.ts +72 -0
- package/dist/core/sync/profile-manager.d.ts.map +1 -0
- package/dist/core/sync/profile-manager.js +338 -0
- package/dist/core/sync/profile-manager.js.map +1 -0
- package/dist/core/sync/profile-selector.d.ts +52 -0
- package/dist/core/sync/profile-selector.d.ts.map +1 -0
- package/dist/core/sync/profile-selector.js +179 -0
- package/dist/core/sync/profile-selector.js.map +1 -0
- package/dist/core/sync/project-context.d.ts +81 -0
- package/dist/core/sync/project-context.d.ts.map +1 -0
- package/dist/core/sync/project-context.js +354 -0
- package/dist/core/sync/project-context.js.map +1 -0
- package/dist/core/sync/rate-limiter.d.ts +116 -0
- package/dist/core/sync/rate-limiter.d.ts.map +1 -0
- package/dist/core/sync/rate-limiter.js +308 -0
- package/dist/core/sync/rate-limiter.js.map +1 -0
- package/dist/core/sync/time-range-selector.d.ts +48 -0
- package/dist/core/sync/time-range-selector.d.ts.map +1 -0
- package/dist/core/sync/time-range-selector.js +224 -0
- package/dist/core/sync/time-range-selector.js.map +1 -0
- package/dist/core/types/config.d.ts +4 -0
- package/dist/core/types/config.d.ts.map +1 -1
- package/dist/core/types/config.js.map +1 -1
- package/dist/core/types/sync-profile.d.ts +205 -0
- package/dist/core/types/sync-profile.d.ts.map +1 -0
- package/dist/core/types/sync-profile.js +8 -0
- package/dist/core/types/sync-profile.js.map +1 -0
- package/dist/utils/project-detection.d.ts +141 -0
- package/dist/utils/project-detection.d.ts.map +1 -0
- package/dist/utils/project-detection.js +321 -0
- package/dist/utils/project-detection.js.map +1 -0
- package/package.json +2 -1
- package/plugins/specweave/agents/pm/AGENT.md +7 -4
- package/plugins/specweave/commands/specweave-abandon.md +17 -17
- package/plugins/specweave/commands/specweave-check-tests.md +14 -14
- package/plugins/specweave/commands/specweave-costs.md +1 -1
- package/plugins/specweave/commands/specweave-do.md +12 -12
- package/plugins/specweave/commands/specweave-done.md +28 -15
- package/plugins/specweave/commands/specweave-import-docs.md +212 -0
- package/plugins/specweave/commands/specweave-increment.md +10 -10
- package/plugins/specweave/commands/specweave-init-multiproject.md +146 -0
- package/plugins/specweave/commands/specweave-next.md +16 -16
- package/plugins/specweave/commands/specweave-pause.md +17 -17
- package/plugins/specweave/commands/specweave-progress.md +10 -10
- package/plugins/specweave/commands/specweave-qa.md +11 -11
- package/plugins/specweave/commands/specweave-resume.md +22 -22
- package/plugins/specweave/commands/specweave-status.md +18 -18
- package/plugins/specweave/commands/specweave-switch-project.md +168 -0
- package/plugins/specweave/commands/specweave-sync-docs.md +1 -1
- package/plugins/specweave/commands/specweave-sync-tasks.md +9 -9
- package/plugins/specweave/commands/specweave-tdd-cycle.md +7 -0
- package/plugins/specweave/commands/specweave-tdd-green.md +7 -0
- package/plugins/specweave/commands/specweave-tdd-red.md +7 -0
- package/plugins/specweave/commands/specweave-tdd-refactor.md +7 -0
- package/plugins/specweave/commands/specweave-translate.md +1 -1
- package/plugins/specweave/commands/specweave-update-scope.md +8 -8
- package/plugins/specweave/commands/specweave-validate.md +18 -20
- package/plugins/specweave/commands/specweave.md +5 -5
- package/plugins/specweave/skills/SKILLS-INDEX.md +1 -1
- package/plugins/specweave/skills/increment-planner/SKILL.md +40 -4
- package/plugins/specweave/skills/increment-quality-judge/SKILL.md +5 -5
- package/plugins/specweave/skills/increment-quality-judge-v2/SKILL.md +5 -5
- package/plugins/specweave/skills/specweave-detector/SKILL.md +3 -3
- package/plugins/specweave-ado/commands/{close-workitem.md → specweave-ado-close-workitem.md} +1 -1
- package/plugins/specweave-ado/commands/{create-workitem.md → specweave-ado-create-workitem.md} +1 -1
- package/plugins/specweave-ado/commands/{status.md → specweave-ado-status.md} +1 -1
- package/plugins/specweave-ado/commands/{sync.md → specweave-ado-sync.md} +1 -1
- package/plugins/specweave-ado/lib/ado-client-v2.ts +547 -0
- package/plugins/specweave-github/commands/{close-issue.md → specweave-github-close-issue.md} +1 -1
- package/plugins/specweave-github/commands/{create-issue.md → specweave-github-create-issue.md} +1 -1
- package/plugins/specweave-github/commands/{status.md → specweave-github-status.md} +1 -1
- package/plugins/specweave-github/commands/{sync-tasks.md → specweave-github-sync-tasks.md} +1 -1
- package/plugins/specweave-github/commands/specweave-github-sync.md +568 -0
- package/plugins/specweave-github/lib/github-client-v2.ts +555 -0
- package/plugins/specweave-infrastructure/commands/{monitor-setup.md → specweave-infrastructure-monitor-setup.md} +1 -1
- package/plugins/specweave-infrastructure/commands/{slo-implement.md → specweave-infrastructure-slo-implement.md} +1 -1
- package/plugins/specweave-jira/commands/{sync.md → specweave-jira-sync.md} +1 -1
- package/plugins/specweave-jira/lib/jira-client-v2.ts +529 -0
- package/plugins/specweave-ml/commands/{ml-deploy.md → specweave-ml-deploy.md} +1 -1
- package/plugins/specweave-ml/commands/{ml-evaluate.md → specweave-ml-evaluate.md} +1 -1
- package/plugins/specweave-ml/commands/{ml-explain.md → specweave-ml-explain.md} +1 -1
- package/plugins/specweave-ml/commands/{ml-pipeline.md → specweave-ml-pipeline.md} +1 -1
- package/src/templates/AGENTS.md.template +1 -0
- package/src/templates/CLAUDE.md.template +1 -0
- package/plugins/specweave-github/commands/sync.md +0 -443
- /package/plugins/specweave/{commands/README.md → COMMANDS.md} +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA REST API Client (Multi-Project Support)
|
|
3
|
+
*
|
|
4
|
+
* Profile-based JIRA client for SpecWeave that supports:
|
|
5
|
+
* - Multiple JIRA projects via sync profiles
|
|
6
|
+
* - Time range filtering with JQL queries
|
|
7
|
+
* - Rate limiting protection
|
|
8
|
+
* - Secure HTTPS requests (no shell injection)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import https from 'https';
|
|
12
|
+
import {
|
|
13
|
+
SyncProfile,
|
|
14
|
+
JiraConfig,
|
|
15
|
+
TimeRangePreset,
|
|
16
|
+
} from '../../../src/core/types/sync-profile';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface JiraIssue {
|
|
23
|
+
id: string;
|
|
24
|
+
key: string;
|
|
25
|
+
self: string;
|
|
26
|
+
fields: {
|
|
27
|
+
summary: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
status: {
|
|
30
|
+
name: string;
|
|
31
|
+
statusCategory: { key: string };
|
|
32
|
+
};
|
|
33
|
+
issuetype: {
|
|
34
|
+
name: string;
|
|
35
|
+
hierarchyLevel?: number;
|
|
36
|
+
};
|
|
37
|
+
created: string;
|
|
38
|
+
updated: string;
|
|
39
|
+
assignee?: { displayName: string };
|
|
40
|
+
reporter?: { displayName: string };
|
|
41
|
+
labels?: string[];
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface JiraSearchResult {
|
|
47
|
+
issues: JiraIssue[];
|
|
48
|
+
total: number;
|
|
49
|
+
maxResults: number;
|
|
50
|
+
startAt: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CreateIssueRequest {
|
|
54
|
+
summary: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
issueType?: string;
|
|
57
|
+
labels?: string[];
|
|
58
|
+
epicLink?: string; // Link to epic (for stories/tasks)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface UpdateIssueRequest {
|
|
62
|
+
summary?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
status?: string;
|
|
65
|
+
labels?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// JIRA Client V2
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export class JiraClientV2 {
|
|
73
|
+
private domain: string;
|
|
74
|
+
private projectKey: string;
|
|
75
|
+
private issueType: string;
|
|
76
|
+
private baseUrl: string;
|
|
77
|
+
private authHeader: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create JIRA client from sync profile
|
|
81
|
+
*/
|
|
82
|
+
constructor(profile: SyncProfile, apiToken: string, email: string) {
|
|
83
|
+
if (profile.provider !== 'jira') {
|
|
84
|
+
throw new Error(`Expected JIRA profile, got ${profile.provider}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = profile.config as JiraConfig;
|
|
88
|
+
this.domain = config.domain;
|
|
89
|
+
this.projectKey = config.projectKey;
|
|
90
|
+
this.issueType = config.issueType || 'Epic';
|
|
91
|
+
|
|
92
|
+
this.baseUrl = `https://${this.domain}/rest/api/3`;
|
|
93
|
+
|
|
94
|
+
// Basic Auth: base64(email:api_token)
|
|
95
|
+
const credentials = `${email}:${apiToken}`;
|
|
96
|
+
this.authHeader =
|
|
97
|
+
'Basic ' + Buffer.from(credentials).toString('base64');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create client from domain/project directly
|
|
102
|
+
*/
|
|
103
|
+
static fromProject(
|
|
104
|
+
domain: string,
|
|
105
|
+
projectKey: string,
|
|
106
|
+
apiToken: string,
|
|
107
|
+
email: string,
|
|
108
|
+
issueType: string = 'Epic'
|
|
109
|
+
): JiraClientV2 {
|
|
110
|
+
const profile: SyncProfile = {
|
|
111
|
+
provider: 'jira',
|
|
112
|
+
displayName: `${domain}/${projectKey}`,
|
|
113
|
+
config: { domain, projectKey, issueType },
|
|
114
|
+
timeRange: { default: '1M', max: '6M' },
|
|
115
|
+
};
|
|
116
|
+
return new JiraClientV2(profile, apiToken, email);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==========================================================================
|
|
120
|
+
// Authentication & Setup
|
|
121
|
+
// ==========================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Test connection and authentication
|
|
125
|
+
*/
|
|
126
|
+
async testConnection(): Promise<{ success: boolean; error?: string }> {
|
|
127
|
+
try {
|
|
128
|
+
await this.request('GET', '/myself');
|
|
129
|
+
return { success: true };
|
|
130
|
+
} catch (error: any) {
|
|
131
|
+
return { success: false, error: error.message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ==========================================================================
|
|
136
|
+
// Issues
|
|
137
|
+
// ==========================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create epic issue
|
|
141
|
+
*/
|
|
142
|
+
async createEpic(request: CreateIssueRequest): Promise<JiraIssue> {
|
|
143
|
+
const payload = {
|
|
144
|
+
fields: {
|
|
145
|
+
project: { key: this.projectKey },
|
|
146
|
+
summary: request.summary,
|
|
147
|
+
description: this.formatDescription(request.description),
|
|
148
|
+
issuetype: { name: this.issueType },
|
|
149
|
+
labels: request.labels || [],
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const response = await this.request('POST', '/issue', payload);
|
|
154
|
+
return await this.getIssue(response.key);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Create story/task linked to epic
|
|
159
|
+
*/
|
|
160
|
+
async createStory(
|
|
161
|
+
request: CreateIssueRequest,
|
|
162
|
+
epicKey: string
|
|
163
|
+
): Promise<JiraIssue> {
|
|
164
|
+
const payload = {
|
|
165
|
+
fields: {
|
|
166
|
+
project: { key: this.projectKey },
|
|
167
|
+
summary: request.summary,
|
|
168
|
+
description: this.formatDescription(request.description),
|
|
169
|
+
issuetype: { name: request.issueType || 'Story' },
|
|
170
|
+
labels: request.labels || [],
|
|
171
|
+
// Epic link (JIRA Cloud uses 'parent' field for epics)
|
|
172
|
+
parent: { key: epicKey },
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const response = await this.request('POST', '/issue', payload);
|
|
177
|
+
return await this.getIssue(response.key);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get issue by key
|
|
182
|
+
*/
|
|
183
|
+
async getIssue(issueKey: string): Promise<JiraIssue> {
|
|
184
|
+
return await this.request('GET', `/issue/${issueKey}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Update issue
|
|
189
|
+
*/
|
|
190
|
+
async updateIssue(
|
|
191
|
+
issueKey: string,
|
|
192
|
+
updates: UpdateIssueRequest
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
const payload: any = { fields: {} };
|
|
195
|
+
|
|
196
|
+
if (updates.summary) {
|
|
197
|
+
payload.fields.summary = updates.summary;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (updates.description !== undefined) {
|
|
201
|
+
payload.fields.description = this.formatDescription(updates.description);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (updates.labels) {
|
|
205
|
+
payload.fields.labels = updates.labels;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await this.request('PUT', `/issue/${issueKey}`, payload);
|
|
209
|
+
|
|
210
|
+
// Status transition (if specified)
|
|
211
|
+
if (updates.status) {
|
|
212
|
+
await this.transitionIssue(issueKey, updates.status);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Transition issue to a different status
|
|
218
|
+
*/
|
|
219
|
+
async transitionIssue(issueKey: string, statusName: string): Promise<void> {
|
|
220
|
+
// Get available transitions
|
|
221
|
+
const transitions = await this.request(
|
|
222
|
+
'GET',
|
|
223
|
+
`/issue/${issueKey}/transitions`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Find transition matching status name
|
|
227
|
+
const transition = transitions.transitions.find(
|
|
228
|
+
(t: any) => t.to.name.toLowerCase() === statusName.toLowerCase()
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (!transition) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Status '${statusName}' not found for issue ${issueKey}. Available: ${transitions.transitions.map((t: any) => t.to.name).join(', ')}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Execute transition
|
|
238
|
+
await this.request('POST', `/issue/${issueKey}/transitions`, {
|
|
239
|
+
transition: { id: transition.id },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add comment to issue
|
|
245
|
+
*/
|
|
246
|
+
async addComment(issueKey: string, comment: string): Promise<void> {
|
|
247
|
+
await this.request('POST', `/issue/${issueKey}/comment`, {
|
|
248
|
+
body: this.formatDescription(comment),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Add labels to issue
|
|
254
|
+
*/
|
|
255
|
+
async addLabels(issueKey: string, labels: string[]): Promise<void> {
|
|
256
|
+
if (labels.length === 0) return;
|
|
257
|
+
|
|
258
|
+
const issue = await this.getIssue(issueKey);
|
|
259
|
+
const existingLabels = issue.fields.labels || [];
|
|
260
|
+
const newLabels = [...new Set([...existingLabels, ...labels])];
|
|
261
|
+
|
|
262
|
+
await this.updateIssue(issueKey, { labels: newLabels });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ==========================================================================
|
|
266
|
+
// Search & Time Range Filtering
|
|
267
|
+
// ==========================================================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Search issues using JQL
|
|
271
|
+
*/
|
|
272
|
+
async searchIssues(
|
|
273
|
+
jql: string,
|
|
274
|
+
options: {
|
|
275
|
+
startAt?: number;
|
|
276
|
+
maxResults?: number;
|
|
277
|
+
fields?: string[];
|
|
278
|
+
} = {}
|
|
279
|
+
): Promise<JiraSearchResult> {
|
|
280
|
+
const params = new URLSearchParams({
|
|
281
|
+
jql,
|
|
282
|
+
startAt: String(options.startAt || 0),
|
|
283
|
+
maxResults: String(options.maxResults || 50),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (options.fields) {
|
|
287
|
+
params.append('fields', options.fields.join(','));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return await this.request('GET', `/search?${params.toString()}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* List issues within time range
|
|
295
|
+
*/
|
|
296
|
+
async listIssuesInTimeRange(
|
|
297
|
+
timeRange: TimeRangePreset,
|
|
298
|
+
customStart?: string,
|
|
299
|
+
customEnd?: string
|
|
300
|
+
): Promise<JiraIssue[]> {
|
|
301
|
+
const { since, until } = this.calculateTimeRange(
|
|
302
|
+
timeRange,
|
|
303
|
+
customStart,
|
|
304
|
+
customEnd
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// JQL query for time range
|
|
308
|
+
const jql = `project = ${this.projectKey} AND created >= "${since}" AND created <= "${until}" ORDER BY created DESC`;
|
|
309
|
+
|
|
310
|
+
const allIssues: JiraIssue[] = [];
|
|
311
|
+
let startAt = 0;
|
|
312
|
+
const maxResults = 100;
|
|
313
|
+
|
|
314
|
+
// Paginate through all results
|
|
315
|
+
while (true) {
|
|
316
|
+
const result = await this.searchIssues(jql, { startAt, maxResults });
|
|
317
|
+
allIssues.push(...result.issues);
|
|
318
|
+
|
|
319
|
+
if (allIssues.length >= result.total) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
startAt += maxResults;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return allIssues;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Calculate date range from preset
|
|
331
|
+
*/
|
|
332
|
+
private calculateTimeRange(
|
|
333
|
+
timeRange: TimeRangePreset,
|
|
334
|
+
customStart?: string,
|
|
335
|
+
customEnd?: string
|
|
336
|
+
): { since: string; until: string } {
|
|
337
|
+
if (timeRange === 'ALL') {
|
|
338
|
+
return {
|
|
339
|
+
since: '1970-01-01',
|
|
340
|
+
until: new Date().toISOString().split('T')[0],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (customStart) {
|
|
345
|
+
return {
|
|
346
|
+
since: customStart,
|
|
347
|
+
until: customEnd || new Date().toISOString().split('T')[0],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const now = new Date();
|
|
352
|
+
const since = new Date(now);
|
|
353
|
+
|
|
354
|
+
switch (timeRange) {
|
|
355
|
+
case '1W':
|
|
356
|
+
since.setDate(now.getDate() - 7);
|
|
357
|
+
break;
|
|
358
|
+
case '2W':
|
|
359
|
+
since.setDate(now.getDate() - 14);
|
|
360
|
+
break;
|
|
361
|
+
case '1M':
|
|
362
|
+
since.setMonth(now.getMonth() - 1);
|
|
363
|
+
break;
|
|
364
|
+
case '3M':
|
|
365
|
+
since.setMonth(now.getMonth() - 3);
|
|
366
|
+
break;
|
|
367
|
+
case '6M':
|
|
368
|
+
since.setMonth(now.getMonth() - 6);
|
|
369
|
+
break;
|
|
370
|
+
case '1Y':
|
|
371
|
+
since.setFullYear(now.getFullYear() - 1);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
since: since.toISOString().split('T')[0],
|
|
377
|
+
until: now.toISOString().split('T')[0],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ==========================================================================
|
|
382
|
+
// Batch Operations
|
|
383
|
+
// ==========================================================================
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Batch create issues with rate limit handling
|
|
387
|
+
*/
|
|
388
|
+
async batchCreateIssues(
|
|
389
|
+
issues: CreateIssueRequest[],
|
|
390
|
+
epicKey?: string,
|
|
391
|
+
options: { batchSize?: number; delayMs?: number } = {}
|
|
392
|
+
): Promise<JiraIssue[]> {
|
|
393
|
+
const { batchSize = 5, delayMs = 12000 } = options; // 5 issues per minute (JIRA: 100/min limit)
|
|
394
|
+
const createdIssues: JiraIssue[] = [];
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < issues.length; i += batchSize) {
|
|
397
|
+
const batch = issues.slice(i, i + batchSize);
|
|
398
|
+
|
|
399
|
+
console.log(
|
|
400
|
+
`Creating issues ${i + 1}-${Math.min(i + batchSize, issues.length)} of ${issues.length}...`
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
for (const issue of batch) {
|
|
404
|
+
try {
|
|
405
|
+
const created = epicKey
|
|
406
|
+
? await this.createStory(issue, epicKey)
|
|
407
|
+
: await this.createEpic(issue);
|
|
408
|
+
|
|
409
|
+
createdIssues.push(created);
|
|
410
|
+
} catch (error: any) {
|
|
411
|
+
console.error(
|
|
412
|
+
`Failed to create issue "${issue.summary}":`,
|
|
413
|
+
error.message
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Delay between batches
|
|
419
|
+
if (i + batchSize < issues.length) {
|
|
420
|
+
console.log(`Waiting ${delayMs / 1000}s to avoid rate limits...`);
|
|
421
|
+
await this.sleep(delayMs);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return createdIssues;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ==========================================================================
|
|
429
|
+
// HTTP Request Handler
|
|
430
|
+
// ==========================================================================
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Make HTTPS request to JIRA API
|
|
434
|
+
*/
|
|
435
|
+
private async request(
|
|
436
|
+
method: string,
|
|
437
|
+
path: string,
|
|
438
|
+
body?: any
|
|
439
|
+
): Promise<any> {
|
|
440
|
+
return new Promise((resolve, reject) => {
|
|
441
|
+
const url = `${this.baseUrl}${path}`;
|
|
442
|
+
const { hostname, pathname, search } = new URL(url);
|
|
443
|
+
|
|
444
|
+
const options = {
|
|
445
|
+
hostname,
|
|
446
|
+
path: pathname + search,
|
|
447
|
+
method,
|
|
448
|
+
headers: {
|
|
449
|
+
Authorization: this.authHeader,
|
|
450
|
+
'Content-Type': 'application/json',
|
|
451
|
+
Accept: 'application/json',
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const req = https.request(options, (res) => {
|
|
456
|
+
let data = '';
|
|
457
|
+
|
|
458
|
+
res.on('data', (chunk) => {
|
|
459
|
+
data += chunk;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
res.on('end', () => {
|
|
463
|
+
// Parse response
|
|
464
|
+
let parsed: any;
|
|
465
|
+
try {
|
|
466
|
+
parsed = data ? JSON.parse(data) : {};
|
|
467
|
+
} catch {
|
|
468
|
+
parsed = { raw: data };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Check status code
|
|
472
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
473
|
+
resolve(parsed);
|
|
474
|
+
} else {
|
|
475
|
+
const errorMsg =
|
|
476
|
+
parsed.errorMessages?.join(', ') ||
|
|
477
|
+
parsed.message ||
|
|
478
|
+
`HTTP ${res.statusCode}: ${data}`;
|
|
479
|
+
reject(new Error(errorMsg));
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
req.on('error', (error) => {
|
|
485
|
+
reject(error);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Send body if present
|
|
489
|
+
if (body) {
|
|
490
|
+
req.write(JSON.stringify(body));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
req.end();
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ==========================================================================
|
|
498
|
+
// Helper Methods
|
|
499
|
+
// ==========================================================================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Format description for JIRA (Atlassian Document Format)
|
|
503
|
+
*/
|
|
504
|
+
private formatDescription(text?: string): any {
|
|
505
|
+
if (!text) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Convert Markdown to Atlassian Document Format (ADF)
|
|
510
|
+
// For now, simple text paragraphs
|
|
511
|
+
return {
|
|
512
|
+
type: 'doc',
|
|
513
|
+
version: 1,
|
|
514
|
+
content: text.split('\n\n').map((paragraph) => ({
|
|
515
|
+
type: 'paragraph',
|
|
516
|
+
content: [
|
|
517
|
+
{
|
|
518
|
+
type: 'text',
|
|
519
|
+
text: paragraph,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
})),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private sleep(ms: number): Promise<void> {
|
|
527
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
@@ -36,6 +36,7 @@ This is a **SpecWeave project** where specifications and documentation are the s
|
|
|
36
36
|
│ └── reports/ # Analysis, completion reports
|
|
37
37
|
├── docs/internal/
|
|
38
38
|
│ ├── strategy/ # Business specs (WHAT, WHY)
|
|
39
|
+
│ ├── specs/ # Feature specifications (detailed requirements)
|
|
39
40
|
│ ├── architecture/ # Technical design (HOW)
|
|
40
41
|
│ ├── delivery/ # Roadmap, CI/CD, guides
|
|
41
42
|
│ ├── operations/ # Runbooks, SLOs
|
|
@@ -231,6 +231,7 @@ Config: Auto-detected from project files
|
|
|
231
231
|
│ ├── docs/ # Strategic documentation
|
|
232
232
|
│ │ ├── internal/
|
|
233
233
|
│ │ │ ├── strategy/ # Business specs (WHAT, WHY)
|
|
234
|
+
│ │ │ ├── specs/ # Feature specifications (detailed requirements)
|
|
234
235
|
│ │ │ ├── architecture/ # Technical design (HOW)
|
|
235
236
|
│ │ │ ├── delivery/ # Guides, roadmap, CI/CD
|
|
236
237
|
│ │ │ ├── operations/ # Runbooks, monitoring
|