specweave 0.7.1 → 0.8.2
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/bin/specweave.js +0 -27
- 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.d.ts.map +1 -1
- package/dist/cli/commands/init.js +4 -93
- 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/hooks/post-increment-plugin-detect.sh +0 -142
- package/plugins/specweave/hooks/pre-task-plugin-detect.sh +0 -96
- package/plugins/specweave/skills/plugin-detector/SKILL.md +0 -324
- package/plugins/specweave-github/commands/sync.md +0 -443
- /package/plugins/specweave/{commands/README.md → COMMANDS.md} +0 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub CLI Wrapper for SpecWeave (Multi-Project Support)
|
|
3
|
+
*
|
|
4
|
+
* Profile-based GitHub client that supports:
|
|
5
|
+
* - Multiple repositories via sync profiles
|
|
6
|
+
* - Time range filtering for syncs
|
|
7
|
+
* - Rate limiting protection
|
|
8
|
+
* - Secure command execution (no shell injection)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
|
|
12
|
+
import { GitHubIssue, GitHubMilestone } from './types';
|
|
13
|
+
import { SyncProfile, GitHubConfig, TimeRangePreset } from '../../../src/core/types/sync-profile';
|
|
14
|
+
|
|
15
|
+
export class GitHubClientV2 {
|
|
16
|
+
private owner: string;
|
|
17
|
+
private repo: string;
|
|
18
|
+
private fullRepo: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create GitHub client from sync profile
|
|
22
|
+
*/
|
|
23
|
+
constructor(profile: SyncProfile) {
|
|
24
|
+
if (profile.provider !== 'github') {
|
|
25
|
+
throw new Error(`Expected GitHub profile, got ${profile.provider}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = profile.config as GitHubConfig;
|
|
29
|
+
this.owner = config.owner;
|
|
30
|
+
this.repo = config.repo;
|
|
31
|
+
this.fullRepo = `${this.owner}/${this.repo}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create client from owner/repo directly
|
|
36
|
+
*/
|
|
37
|
+
static fromRepo(owner: string, repo: string): GitHubClientV2 {
|
|
38
|
+
const profile: SyncProfile = {
|
|
39
|
+
provider: 'github',
|
|
40
|
+
displayName: `${owner}/${repo}`,
|
|
41
|
+
config: { owner, repo },
|
|
42
|
+
timeRange: { default: '1M', max: '6M' },
|
|
43
|
+
};
|
|
44
|
+
return new GitHubClientV2(profile);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ==========================================================================
|
|
48
|
+
// Authentication & Setup
|
|
49
|
+
// ==========================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if GitHub CLI is installed and authenticated
|
|
53
|
+
*/
|
|
54
|
+
static async checkCLI(): Promise<{
|
|
55
|
+
installed: boolean;
|
|
56
|
+
authenticated: boolean;
|
|
57
|
+
error?: string;
|
|
58
|
+
}> {
|
|
59
|
+
// Check installation
|
|
60
|
+
const versionCheck = await execFileNoThrow('gh', ['--version']);
|
|
61
|
+
if (versionCheck.status !== 0) {
|
|
62
|
+
return {
|
|
63
|
+
installed: false,
|
|
64
|
+
authenticated: false,
|
|
65
|
+
error: 'GitHub CLI (gh) not installed. Install from: https://cli.github.com/',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check authentication
|
|
70
|
+
const authCheck = await execFileNoThrow('gh', ['auth', 'status']);
|
|
71
|
+
if (authCheck.status !== 0) {
|
|
72
|
+
return {
|
|
73
|
+
installed: true,
|
|
74
|
+
authenticated: false,
|
|
75
|
+
error: 'GitHub CLI not authenticated. Run: gh auth login',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { installed: true, authenticated: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Auto-detect repository from git remote
|
|
84
|
+
*/
|
|
85
|
+
static async detectRepo(cwd?: string): Promise<{owner: string; repo: string} | null> {
|
|
86
|
+
const result = await execFileNoThrow('git', [
|
|
87
|
+
'remote',
|
|
88
|
+
'get-url',
|
|
89
|
+
'origin',
|
|
90
|
+
], { cwd });
|
|
91
|
+
|
|
92
|
+
if (result.status !== 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const remote = result.stdout.trim();
|
|
97
|
+
const match = remote.match(/github\.com[:/](.+)\/(.+?)(?:\.git)?$/);
|
|
98
|
+
|
|
99
|
+
if (!match) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
owner: match[1],
|
|
105
|
+
repo: match[2],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ==========================================================================
|
|
110
|
+
// Milestones
|
|
111
|
+
// ==========================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create or get existing milestone
|
|
115
|
+
*/
|
|
116
|
+
async createOrGetMilestone(
|
|
117
|
+
title: string,
|
|
118
|
+
description?: string,
|
|
119
|
+
daysFromNow: number = 2
|
|
120
|
+
): Promise<GitHubMilestone> {
|
|
121
|
+
// Check if milestone already exists
|
|
122
|
+
const existing = await this.getMilestoneByTitle(title);
|
|
123
|
+
if (existing) {
|
|
124
|
+
return existing;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Calculate due date
|
|
128
|
+
const dueDate = new Date();
|
|
129
|
+
dueDate.setDate(dueDate.getDate() + daysFromNow);
|
|
130
|
+
const dueDateISO = dueDate.toISOString();
|
|
131
|
+
|
|
132
|
+
// Build API request
|
|
133
|
+
const args = [
|
|
134
|
+
'api',
|
|
135
|
+
`repos/${this.fullRepo}/milestones`,
|
|
136
|
+
'-f',
|
|
137
|
+
`title=${title}`,
|
|
138
|
+
'-f',
|
|
139
|
+
`due_on=${dueDateISO}`,
|
|
140
|
+
'--jq',
|
|
141
|
+
'{number: .number, title: .title, description: .description, state: .state, due_on: .due_on}',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
if (description) {
|
|
145
|
+
args.splice(4, 0, '-f', `description=${description}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = await execFileNoThrow('gh', args);
|
|
149
|
+
|
|
150
|
+
if (result.status !== 0) {
|
|
151
|
+
throw new Error(`Failed to create milestone: ${result.stderr || result.stdout}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return JSON.parse(result.stdout);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get milestone by title
|
|
159
|
+
*/
|
|
160
|
+
private async getMilestoneByTitle(
|
|
161
|
+
title: string
|
|
162
|
+
): Promise<GitHubMilestone | null> {
|
|
163
|
+
const result = await execFileNoThrow('gh', [
|
|
164
|
+
'api',
|
|
165
|
+
`repos/${this.fullRepo}/milestones`,
|
|
166
|
+
'--jq',
|
|
167
|
+
`.[] | select(.title=="${title}") | {number: .number, title: .title, description: .description, state: .state}`,
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return JSON.parse(result.stdout);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ==========================================================================
|
|
178
|
+
// Issues
|
|
179
|
+
// ==========================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create epic issue (increment-level)
|
|
183
|
+
*/
|
|
184
|
+
async createEpicIssue(
|
|
185
|
+
title: string,
|
|
186
|
+
body: string,
|
|
187
|
+
milestone?: number | string,
|
|
188
|
+
labels: string[] = []
|
|
189
|
+
): Promise<GitHubIssue> {
|
|
190
|
+
const args = [
|
|
191
|
+
'issue',
|
|
192
|
+
'create',
|
|
193
|
+
'--repo',
|
|
194
|
+
this.fullRepo,
|
|
195
|
+
'--title',
|
|
196
|
+
title,
|
|
197
|
+
'--body',
|
|
198
|
+
body,
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
// Add labels
|
|
202
|
+
for (const label of labels) {
|
|
203
|
+
args.push('--label', label);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add milestone
|
|
207
|
+
if (milestone !== undefined) {
|
|
208
|
+
args.push('--milestone', String(milestone));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create issue (returns URL)
|
|
212
|
+
const createResult = await execFileNoThrow('gh', args);
|
|
213
|
+
|
|
214
|
+
if (createResult.status !== 0) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Failed to create epic issue: ${createResult.stderr || createResult.stdout}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const issueUrl = createResult.stdout.trim();
|
|
221
|
+
const issueNumber = parseInt(issueUrl.split('/').pop() || '0', 10);
|
|
222
|
+
|
|
223
|
+
if (!issueNumber) {
|
|
224
|
+
throw new Error(`Failed to extract issue number from URL: ${issueUrl}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Fetch issue details
|
|
228
|
+
return await this.getIssue(issueNumber);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create task issue (linked to epic)
|
|
233
|
+
*/
|
|
234
|
+
async createTaskIssue(
|
|
235
|
+
title: string,
|
|
236
|
+
body: string,
|
|
237
|
+
epicNumber: number,
|
|
238
|
+
milestone?: number | string,
|
|
239
|
+
labels: string[] = []
|
|
240
|
+
): Promise<GitHubIssue> {
|
|
241
|
+
// Add epic reference to body
|
|
242
|
+
const enhancedBody = `**Part of**: #${epicNumber}\n\n${body}`;
|
|
243
|
+
|
|
244
|
+
return await this.createEpicIssue(title, enhancedBody, milestone, labels);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get issue details
|
|
249
|
+
*/
|
|
250
|
+
async getIssue(issueNumber: number): Promise<GitHubIssue> {
|
|
251
|
+
const result = await execFileNoThrow('gh', [
|
|
252
|
+
'issue',
|
|
253
|
+
'view',
|
|
254
|
+
String(issueNumber),
|
|
255
|
+
'--repo',
|
|
256
|
+
this.fullRepo,
|
|
257
|
+
'--json',
|
|
258
|
+
'number,title,body,state,url,labels,milestone',
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
if (result.status !== 0) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Failed to get issue #${issueNumber}: ${result.stderr || result.stdout}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const issue = JSON.parse(result.stdout);
|
|
268
|
+
return {
|
|
269
|
+
...issue,
|
|
270
|
+
html_url: issue.url,
|
|
271
|
+
labels: issue.labels?.map((l: any) => l.name) || [],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Update issue body
|
|
277
|
+
*/
|
|
278
|
+
async updateIssueBody(issueNumber: number, newBody: string): Promise<void> {
|
|
279
|
+
const result = await execFileNoThrow('gh', [
|
|
280
|
+
'issue',
|
|
281
|
+
'edit',
|
|
282
|
+
String(issueNumber),
|
|
283
|
+
'--repo',
|
|
284
|
+
this.fullRepo,
|
|
285
|
+
'--body',
|
|
286
|
+
newBody,
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
if (result.status !== 0) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Failed to update issue #${issueNumber}: ${result.stderr || result.stdout}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Close issue
|
|
298
|
+
*/
|
|
299
|
+
async closeIssue(issueNumber: number, comment?: string): Promise<void> {
|
|
300
|
+
if (comment) {
|
|
301
|
+
await this.addComment(issueNumber, comment);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const result = await execFileNoThrow('gh', [
|
|
305
|
+
'issue',
|
|
306
|
+
'close',
|
|
307
|
+
String(issueNumber),
|
|
308
|
+
'--repo',
|
|
309
|
+
this.fullRepo,
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
if (result.status !== 0) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Failed to close issue #${issueNumber}: ${result.stderr || result.stdout}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Add comment to issue
|
|
321
|
+
*/
|
|
322
|
+
async addComment(issueNumber: number, comment: string): Promise<void> {
|
|
323
|
+
const result = await execFileNoThrow('gh', [
|
|
324
|
+
'issue',
|
|
325
|
+
'comment',
|
|
326
|
+
String(issueNumber),
|
|
327
|
+
'--repo',
|
|
328
|
+
this.fullRepo,
|
|
329
|
+
'--body',
|
|
330
|
+
comment,
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
if (result.status !== 0) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Failed to add comment to issue #${issueNumber}: ${result.stderr || result.stdout}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Add labels to issue
|
|
342
|
+
*/
|
|
343
|
+
async addLabels(issueNumber: number, labels: string[]): Promise<void> {
|
|
344
|
+
if (labels.length === 0) return;
|
|
345
|
+
|
|
346
|
+
const args = [
|
|
347
|
+
'issue',
|
|
348
|
+
'edit',
|
|
349
|
+
String(issueNumber),
|
|
350
|
+
'--repo',
|
|
351
|
+
this.fullRepo,
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
for (const label of labels) {
|
|
355
|
+
args.push('--add-label', label);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const result = await execFileNoThrow('gh', args);
|
|
359
|
+
|
|
360
|
+
if (result.status !== 0) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Failed to add labels to issue #${issueNumber}: ${result.stderr || result.stdout}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ==========================================================================
|
|
368
|
+
// Time Range Filtering
|
|
369
|
+
// ==========================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* List issues within a time range
|
|
373
|
+
*/
|
|
374
|
+
async listIssuesInTimeRange(
|
|
375
|
+
timeRange: TimeRangePreset,
|
|
376
|
+
customStart?: string,
|
|
377
|
+
customEnd?: string
|
|
378
|
+
): Promise<GitHubIssue[]> {
|
|
379
|
+
const { since, until } = this.calculateTimeRange(timeRange, customStart, customEnd);
|
|
380
|
+
|
|
381
|
+
// GitHub search query
|
|
382
|
+
const query = `repo:${this.fullRepo} is:issue created:${since}..${until}`;
|
|
383
|
+
|
|
384
|
+
const result = await execFileNoThrow('gh', [
|
|
385
|
+
'search',
|
|
386
|
+
'issues',
|
|
387
|
+
query,
|
|
388
|
+
'--json',
|
|
389
|
+
'number,title,body,state,url,labels,milestone',
|
|
390
|
+
'--limit',
|
|
391
|
+
'1000', // Max results
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
if (result.status !== 0) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Failed to list issues: ${result.stderr || result.stdout}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const issues = JSON.parse(result.stdout);
|
|
401
|
+
return issues.map((issue: any) => ({
|
|
402
|
+
...issue,
|
|
403
|
+
html_url: issue.url,
|
|
404
|
+
labels: issue.labels?.map((l: any) => l.name) || [],
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Calculate date range from time range preset
|
|
410
|
+
*/
|
|
411
|
+
private calculateTimeRange(
|
|
412
|
+
timeRange: TimeRangePreset,
|
|
413
|
+
customStart?: string,
|
|
414
|
+
customEnd?: string
|
|
415
|
+
): { since: string; until: string } {
|
|
416
|
+
if (timeRange === 'ALL') {
|
|
417
|
+
return {
|
|
418
|
+
since: '1970-01-01',
|
|
419
|
+
until: new Date().toISOString().split('T')[0],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (customStart) {
|
|
424
|
+
return {
|
|
425
|
+
since: customStart,
|
|
426
|
+
until: customEnd || new Date().toISOString().split('T')[0],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const now = new Date();
|
|
431
|
+
const since = new Date(now);
|
|
432
|
+
|
|
433
|
+
// Calculate date based on preset
|
|
434
|
+
switch (timeRange) {
|
|
435
|
+
case '1W':
|
|
436
|
+
since.setDate(now.getDate() - 7);
|
|
437
|
+
break;
|
|
438
|
+
case '2W':
|
|
439
|
+
since.setDate(now.getDate() - 14);
|
|
440
|
+
break;
|
|
441
|
+
case '1M':
|
|
442
|
+
since.setMonth(now.getMonth() - 1);
|
|
443
|
+
break;
|
|
444
|
+
case '3M':
|
|
445
|
+
since.setMonth(now.getMonth() - 3);
|
|
446
|
+
break;
|
|
447
|
+
case '6M':
|
|
448
|
+
since.setMonth(now.getMonth() - 6);
|
|
449
|
+
break;
|
|
450
|
+
case '1Y':
|
|
451
|
+
since.setFullYear(now.getFullYear() - 1);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
since: since.toISOString().split('T')[0],
|
|
457
|
+
until: now.toISOString().split('T')[0],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ==========================================================================
|
|
462
|
+
// Rate Limiting
|
|
463
|
+
// ==========================================================================
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Check rate limit status
|
|
467
|
+
*/
|
|
468
|
+
async checkRateLimit(): Promise<{
|
|
469
|
+
remaining: number;
|
|
470
|
+
limit: number;
|
|
471
|
+
reset: Date;
|
|
472
|
+
}> {
|
|
473
|
+
const result = await execFileNoThrow('gh', [
|
|
474
|
+
'api',
|
|
475
|
+
'rate_limit',
|
|
476
|
+
'--jq',
|
|
477
|
+
'.rate | {remaining: .remaining, limit: .limit, reset: .reset}',
|
|
478
|
+
]);
|
|
479
|
+
|
|
480
|
+
if (result.status !== 0) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
`Failed to check rate limit: ${result.stderr || result.stdout}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const data = JSON.parse(result.stdout);
|
|
487
|
+
return {
|
|
488
|
+
...data,
|
|
489
|
+
reset: new Date(data.reset * 1000),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ==========================================================================
|
|
494
|
+
// Batch Operations
|
|
495
|
+
// ==========================================================================
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Batch create issues with rate limit handling
|
|
499
|
+
*/
|
|
500
|
+
async batchCreateIssues(
|
|
501
|
+
issues: Array<{ title: string; body: string; labels?: string[] }>,
|
|
502
|
+
milestone?: number | string,
|
|
503
|
+
epicNumber?: number,
|
|
504
|
+
options: { batchSize?: number; delayMs?: number } = {}
|
|
505
|
+
): Promise<GitHubIssue[]> {
|
|
506
|
+
const { batchSize = 10, delayMs = 6000 } = options;
|
|
507
|
+
const createdIssues: GitHubIssue[] = [];
|
|
508
|
+
|
|
509
|
+
for (let i = 0; i < issues.length; i += batchSize) {
|
|
510
|
+
const batch = issues.slice(i, i + batchSize);
|
|
511
|
+
|
|
512
|
+
console.log(
|
|
513
|
+
`Creating issues ${i + 1}-${Math.min(i + batchSize, issues.length)} of ${issues.length}...`
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
for (const issue of batch) {
|
|
517
|
+
try {
|
|
518
|
+
const created = epicNumber
|
|
519
|
+
? await this.createTaskIssue(
|
|
520
|
+
issue.title,
|
|
521
|
+
issue.body,
|
|
522
|
+
epicNumber,
|
|
523
|
+
milestone,
|
|
524
|
+
issue.labels
|
|
525
|
+
)
|
|
526
|
+
: await this.createEpicIssue(
|
|
527
|
+
issue.title,
|
|
528
|
+
issue.body,
|
|
529
|
+
milestone,
|
|
530
|
+
issue.labels
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
createdIssues.push(created);
|
|
534
|
+
} catch (error: any) {
|
|
535
|
+
console.error(
|
|
536
|
+
`Failed to create issue "${issue.title}":`,
|
|
537
|
+
error.message
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Delay between batches
|
|
543
|
+
if (i + batchSize < issues.length) {
|
|
544
|
+
console.log(`Waiting ${delayMs / 1000}s to avoid rate limits...`);
|
|
545
|
+
await this.sleep(delayMs);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return createdIssues;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private sleep(ms: number): Promise<void> {
|
|
553
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
554
|
+
}
|
|
555
|
+
}
|