@agentic15.com/agentic15-claude-zen 1.0.1 ā 2.0.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/CHANGELOG.md +60 -60
- package/bin/agentic15.js +50 -0
- package/dist/index.js +8 -8
- package/dist/index.js.map +3 -3
- package/package.json +66 -61
- package/src/cli/AuthCommand.js +195 -0
- package/src/cli/CommitCommand.js +290 -0
- package/src/cli/PlanCommand.js +123 -0
- package/src/cli/StatusCommand.js +156 -0
- package/src/cli/TaskCommand.js +249 -0
- package/src/core/GitHubClient.js +170 -0
- package/src/core/GitHubConfig.js +192 -0
- package/src/core/HookInstaller.js +71 -54
- package/src/core/TaskIssueMapper.js +137 -0
- package/src/utils/updateTaskGitHubStatus.js +81 -0
- package/templates/.claude/POST-INSTALL.md +18 -248
- package/templates/.claude/hooks/complete-task.js +224 -0
- package/templates/.claude/hooks/post-merge.js +163 -0
- package/templates/.claude/hooks/start-task.js +233 -0
- package/templates/.claude/settings.json +279 -262
- package/templates/.claude/settings.local.json.example +11 -0
- package/templates/package.json +6 -15
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export class StatusCommand {
|
|
6
|
+
static show() {
|
|
7
|
+
console.log('\nš Project Status\n');
|
|
8
|
+
|
|
9
|
+
// Load tracker
|
|
10
|
+
const tracker = this.loadTracker();
|
|
11
|
+
|
|
12
|
+
if (!tracker) {
|
|
13
|
+
console.log('ā No active plan found\n');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Calculate statistics
|
|
18
|
+
const completed = tracker.taskFiles.filter(t => t.status === 'completed');
|
|
19
|
+
const inProgress = tracker.taskFiles.filter(t => t.status === 'in_progress');
|
|
20
|
+
const pending = tracker.taskFiles.filter(t => t.status === 'pending');
|
|
21
|
+
const blocked = tracker.taskFiles.filter(t => t.status === 'blocked');
|
|
22
|
+
const total = tracker.taskFiles.length;
|
|
23
|
+
|
|
24
|
+
// Display plan info
|
|
25
|
+
console.log(` Plan: ${tracker.planId}`);
|
|
26
|
+
console.log(` Total Tasks: ${total}\n`);
|
|
27
|
+
|
|
28
|
+
// Progress bar
|
|
29
|
+
const completedPercent = Math.round((completed.length / total) * 100);
|
|
30
|
+
const barLength = 30;
|
|
31
|
+
const filledLength = Math.round((completedPercent / 100) * barLength);
|
|
32
|
+
const bar = 'ā'.repeat(filledLength) + 'ā'.repeat(barLength - filledLength);
|
|
33
|
+
|
|
34
|
+
console.log(` Progress: [${bar}] ${completedPercent}%\n`);
|
|
35
|
+
|
|
36
|
+
// Status breakdown
|
|
37
|
+
console.log(' Status Breakdown:');
|
|
38
|
+
console.log(` ā
Completed: ${completed.length}`);
|
|
39
|
+
console.log(` š In Progress: ${inProgress.length}`);
|
|
40
|
+
console.log(` ā³ Pending: ${pending.length}`);
|
|
41
|
+
|
|
42
|
+
if (blocked.length > 0) {
|
|
43
|
+
console.log(` š« Blocked: ${blocked.length}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
|
|
48
|
+
// Current task
|
|
49
|
+
if (inProgress.length > 0) {
|
|
50
|
+
const current = inProgress[0];
|
|
51
|
+
console.log(' š Current Task:');
|
|
52
|
+
console.log(` ${current.id}: ${current.title}`);
|
|
53
|
+
|
|
54
|
+
// Show changed files
|
|
55
|
+
try {
|
|
56
|
+
const diff = execSync('git diff --name-only', { encoding: 'utf-8' });
|
|
57
|
+
const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' });
|
|
58
|
+
|
|
59
|
+
const allChanges = new Set([
|
|
60
|
+
...diff.trim().split('\n').filter(Boolean),
|
|
61
|
+
...staged.trim().split('\n').filter(Boolean)
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
if (allChanges.size > 0) {
|
|
65
|
+
console.log(`\n š Modified files (${allChanges.size}):`);
|
|
66
|
+
Array.from(allChanges).slice(0, 10).forEach(file => {
|
|
67
|
+
console.log(` - ${file}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (allChanges.size > 10) {
|
|
71
|
+
console.log(` ... and ${allChanges.size - 10} more`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Ignore git errors
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(' š” Next step: agentic15 commit');
|
|
80
|
+
} else if (pending.length > 0) {
|
|
81
|
+
console.log(' š Next Task:');
|
|
82
|
+
const next = pending[0];
|
|
83
|
+
console.log(` ${next.id}: ${next.title}`);
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(' š” Next step: agentic15 task next');
|
|
86
|
+
} else if (blocked.length > 0) {
|
|
87
|
+
console.log(' š« Blocked Tasks:');
|
|
88
|
+
blocked.slice(0, 3).forEach(task => {
|
|
89
|
+
console.log(` ${task.id}: ${task.title}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (blocked.length > 3) {
|
|
93
|
+
console.log(` ... and ${blocked.length - 3} more`);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
console.log(' š All tasks completed!');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('');
|
|
100
|
+
|
|
101
|
+
// Recent activity
|
|
102
|
+
const recentCompleted = completed
|
|
103
|
+
.filter(t => t.completedAt)
|
|
104
|
+
.sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt))
|
|
105
|
+
.slice(0, 3);
|
|
106
|
+
|
|
107
|
+
if (recentCompleted.length > 0) {
|
|
108
|
+
console.log(' š Recently Completed:');
|
|
109
|
+
recentCompleted.forEach(task => {
|
|
110
|
+
const date = new Date(task.completedAt);
|
|
111
|
+
const timeAgo = this.getTimeAgo(date);
|
|
112
|
+
console.log(` ā ${task.id}: ${task.title} (${timeAgo})`);
|
|
113
|
+
});
|
|
114
|
+
console.log('');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static loadTracker() {
|
|
119
|
+
const activePlanPath = join(process.cwd(), '.claude', 'ACTIVE-PLAN');
|
|
120
|
+
|
|
121
|
+
if (!existsSync(activePlanPath)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const planId = readFileSync(activePlanPath, 'utf-8').trim();
|
|
126
|
+
const trackerPath = join(process.cwd(), '.claude', 'plans', planId, 'TASK-TRACKER.json');
|
|
127
|
+
|
|
128
|
+
if (!existsSync(trackerPath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return JSON.parse(readFileSync(trackerPath, 'utf-8'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static getTimeAgo(date) {
|
|
136
|
+
const seconds = Math.floor((new Date() - date) / 1000);
|
|
137
|
+
|
|
138
|
+
const intervals = {
|
|
139
|
+
year: 31536000,
|
|
140
|
+
month: 2592000,
|
|
141
|
+
week: 604800,
|
|
142
|
+
day: 86400,
|
|
143
|
+
hour: 3600,
|
|
144
|
+
minute: 60
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
148
|
+
const interval = Math.floor(seconds / secondsInUnit);
|
|
149
|
+
if (interval >= 1) {
|
|
150
|
+
return `${interval} ${unit}${interval === 1 ? '' : 's'} ago`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return 'just now';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { GitHubClient } from '../core/GitHubClient.js';
|
|
5
|
+
import { GitHubConfig } from '../core/GitHubConfig.js';
|
|
6
|
+
import { TaskIssueMapper } from '../core/TaskIssueMapper.js';
|
|
7
|
+
|
|
8
|
+
export class TaskCommand {
|
|
9
|
+
static async handle(action, taskId) {
|
|
10
|
+
switch (action) {
|
|
11
|
+
case 'start':
|
|
12
|
+
return this.startTask(taskId);
|
|
13
|
+
case 'next':
|
|
14
|
+
return this.startNext();
|
|
15
|
+
case 'status':
|
|
16
|
+
return this.showStatus();
|
|
17
|
+
default:
|
|
18
|
+
console.log(`\nā Unknown action: ${action}`);
|
|
19
|
+
console.log(' Valid actions: start, next, status\n');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static async startTask(taskId) {
|
|
25
|
+
if (!taskId) {
|
|
26
|
+
console.log('\nā Task ID required for "start" action');
|
|
27
|
+
console.log(' Usage: agentic15 task start TASK-001\n');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Load task tracker
|
|
32
|
+
const tracker = this.loadTracker();
|
|
33
|
+
const task = tracker.taskFiles.find(t => t.id === taskId);
|
|
34
|
+
|
|
35
|
+
if (!task) {
|
|
36
|
+
console.log(`\nā Task not found: ${taskId}\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (task.status === 'completed') {
|
|
41
|
+
console.log(`\nā ļø Task ${taskId} is already completed\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if another task is in progress
|
|
46
|
+
const inProgress = tracker.taskFiles.find(t => t.status === 'in_progress');
|
|
47
|
+
if (inProgress && inProgress.id !== taskId) {
|
|
48
|
+
console.log(`\nā ļø Task ${inProgress.id} is already in progress`);
|
|
49
|
+
console.log(` Complete it first with: agentic15 commit\n`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Create feature branch
|
|
54
|
+
const branchName = `feature/${taskId.toLowerCase()}`;
|
|
55
|
+
try {
|
|
56
|
+
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Branch might already exist
|
|
59
|
+
try {
|
|
60
|
+
execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.log(`\nā Failed to create/checkout branch: ${branchName}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update task status
|
|
68
|
+
task.status = 'in_progress';
|
|
69
|
+
task.startedAt = new Date().toISOString();
|
|
70
|
+
this.saveTracker(tracker);
|
|
71
|
+
|
|
72
|
+
// Create GitHub issue if enabled
|
|
73
|
+
let githubIssue = null;
|
|
74
|
+
const config = new GitHubConfig(process.cwd());
|
|
75
|
+
if (config.isAutoCreateEnabled()) {
|
|
76
|
+
githubIssue = await this.createGitHubIssue(task, config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Display task details
|
|
80
|
+
this.displayTaskDetails(task, githubIssue, tracker);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static async startNext() {
|
|
84
|
+
const tracker = this.loadTracker();
|
|
85
|
+
|
|
86
|
+
// Find next pending task
|
|
87
|
+
const nextTask = tracker.taskFiles.find(t => t.status === 'pending');
|
|
88
|
+
|
|
89
|
+
if (!nextTask) {
|
|
90
|
+
console.log('\nā
No more pending tasks!\n');
|
|
91
|
+
const completed = tracker.taskFiles.filter(t => t.status === 'completed').length;
|
|
92
|
+
const total = tracker.taskFiles.length;
|
|
93
|
+
console.log(` Progress: ${completed}/${total} tasks completed\n`);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\nā¶ļø Auto-starting next task: ${nextTask.id}\n`);
|
|
98
|
+
return this.startTask(nextTask.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static showStatus() {
|
|
102
|
+
const tracker = this.loadTracker();
|
|
103
|
+
|
|
104
|
+
const inProgress = tracker.taskFiles.find(t => t.status === 'in_progress');
|
|
105
|
+
const completed = tracker.taskFiles.filter(t => t.status === 'completed').length;
|
|
106
|
+
const pending = tracker.taskFiles.filter(t => t.status === 'pending').length;
|
|
107
|
+
const total = tracker.taskFiles.length;
|
|
108
|
+
|
|
109
|
+
console.log('\nš Task Status\n');
|
|
110
|
+
console.log(` Plan: ${tracker.planId}`);
|
|
111
|
+
console.log(` Progress: ${completed}/${total} completed (${pending} pending)\n`);
|
|
112
|
+
|
|
113
|
+
if (inProgress) {
|
|
114
|
+
console.log(` š Currently working on: ${inProgress.id}`);
|
|
115
|
+
console.log(` š ${inProgress.title}`);
|
|
116
|
+
|
|
117
|
+
// Show changed files
|
|
118
|
+
try {
|
|
119
|
+
const diff = execSync('git diff --name-only', { encoding: 'utf-8' });
|
|
120
|
+
if (diff.trim()) {
|
|
121
|
+
console.log(`\n š Changed files:`);
|
|
122
|
+
diff.trim().split('\n').forEach(file => {
|
|
123
|
+
console.log(` - ${file}`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// Ignore
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
console.log(' ā¹ļø No task currently in progress');
|
|
131
|
+
console.log(` Run: agentic15 task next\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static async createGitHubIssue(task, config) {
|
|
138
|
+
try {
|
|
139
|
+
const client = new GitHubClient(
|
|
140
|
+
config.getToken(),
|
|
141
|
+
config.getRepoInfo().owner,
|
|
142
|
+
config.getRepoInfo().repo
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!client.isConfigured()) {
|
|
146
|
+
console.log('\nā ļø GitHub not configured. Skipping issue creation.');
|
|
147
|
+
console.log(' Run: agentic15 auth setup\n');
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Load full task details
|
|
152
|
+
const taskPath = this.getTaskPath(task.id);
|
|
153
|
+
const taskData = JSON.parse(readFileSync(taskPath, 'utf-8'));
|
|
154
|
+
|
|
155
|
+
const { title, body, labels } = TaskIssueMapper.mapTaskToIssue(taskData);
|
|
156
|
+
const issueNumber = await client.createIssue(title, body, labels);
|
|
157
|
+
|
|
158
|
+
if (issueNumber) {
|
|
159
|
+
// Save issue number to task
|
|
160
|
+
taskData.githubIssue = issueNumber;
|
|
161
|
+
writeFileSync(taskPath, JSON.stringify(taskData, null, 2));
|
|
162
|
+
|
|
163
|
+
console.log(`\nā Created GitHub issue #${issueNumber}`);
|
|
164
|
+
const repoInfo = config.getRepoInfo();
|
|
165
|
+
console.log(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/issues/${issueNumber}\n`);
|
|
166
|
+
|
|
167
|
+
return issueNumber;
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.log(`\nā ļø Failed to create GitHub issue: ${error.message}\n`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static displayTaskDetails(task, githubIssue, tracker) {
|
|
177
|
+
console.log(`\nā
Started task: ${task.id}`);
|
|
178
|
+
console.log(`š Plan: ${tracker.planId}\n`);
|
|
179
|
+
console.log(`š ${task.title}`);
|
|
180
|
+
|
|
181
|
+
if (task.description) {
|
|
182
|
+
console.log(`š ${task.description}\n`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (task.phase) {
|
|
186
|
+
console.log(`š§ Phase: ${task.phase}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (githubIssue) {
|
|
190
|
+
const config = new GitHubConfig(process.cwd());
|
|
191
|
+
const repoInfo = config.getRepoInfo();
|
|
192
|
+
console.log(`š GitHub Issue: https://github.com/${repoInfo.owner}/${repoInfo.repo}/issues/${githubIssue}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Load full task for completion criteria
|
|
196
|
+
try {
|
|
197
|
+
const taskPath = this.getTaskPath(task.id);
|
|
198
|
+
const taskData = JSON.parse(readFileSync(taskPath, 'utf-8'));
|
|
199
|
+
|
|
200
|
+
if (taskData.completionCriteria && taskData.completionCriteria.length > 0) {
|
|
201
|
+
console.log(`\nā Completion criteria:`);
|
|
202
|
+
taskData.completionCriteria.forEach((criteria, idx) => {
|
|
203
|
+
console.log(` ${idx + 1}. ${criteria}`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// Ignore
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(`\nš” Next steps:`);
|
|
211
|
+
console.log(` 1. Tell Claude: "Write code for ${task.id}"`);
|
|
212
|
+
console.log(` 2. When done: agentic15 commit\n`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static loadTracker() {
|
|
216
|
+
const activePlanPath = join(process.cwd(), '.claude', 'ACTIVE-PLAN');
|
|
217
|
+
|
|
218
|
+
if (!existsSync(activePlanPath)) {
|
|
219
|
+
console.log('\nā No active plan found');
|
|
220
|
+
console.log(' Run: agentic15 plan "project description"\n');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const planId = readFileSync(activePlanPath, 'utf-8').trim();
|
|
225
|
+
const trackerPath = join(process.cwd(), '.claude', 'plans', planId, 'TASK-TRACKER.json');
|
|
226
|
+
|
|
227
|
+
if (!existsSync(trackerPath)) {
|
|
228
|
+
console.log('\nā Task tracker not found');
|
|
229
|
+
console.log(' Run: agentic15 plan\n');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return JSON.parse(readFileSync(trackerPath, 'utf-8'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static saveTracker(tracker) {
|
|
237
|
+
const activePlanPath = join(process.cwd(), '.claude', 'ACTIVE-PLAN');
|
|
238
|
+
const planId = readFileSync(activePlanPath, 'utf-8').trim();
|
|
239
|
+
const trackerPath = join(process.cwd(), '.claude', 'plans', planId, 'TASK-TRACKER.json');
|
|
240
|
+
|
|
241
|
+
writeFileSync(trackerPath, JSON.stringify(tracker, null, 2));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static getTaskPath(taskId) {
|
|
245
|
+
const activePlanPath = join(process.cwd(), '.claude', 'ACTIVE-PLAN');
|
|
246
|
+
const planId = readFileSync(activePlanPath, 'utf-8').trim();
|
|
247
|
+
return join(process.cwd(), '.claude', 'plans', planId, 'tasks', `${taskId}.json`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024-2025 agentic15.com
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Octokit } from '@octokit/rest';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GitHubClient - Handles all GitHub API interactions
|
|
21
|
+
*
|
|
22
|
+
* Single Responsibility: Communicate with GitHub Issues API
|
|
23
|
+
*
|
|
24
|
+
* This class encapsulates all GitHub API calls and provides graceful
|
|
25
|
+
* degradation when GitHub integration is unavailable or disabled.
|
|
26
|
+
*/
|
|
27
|
+
export class GitHubClient {
|
|
28
|
+
/**
|
|
29
|
+
* Initialize GitHub client
|
|
30
|
+
*
|
|
31
|
+
* @param {string|null} token - GitHub Personal Access Token
|
|
32
|
+
* @param {string|null} owner - Repository owner
|
|
33
|
+
* @param {string|null} repo - Repository name
|
|
34
|
+
*/
|
|
35
|
+
constructor(token, owner, repo) {
|
|
36
|
+
if (!token || !owner || !repo) {
|
|
37
|
+
this.configured = false;
|
|
38
|
+
this.octokit = null;
|
|
39
|
+
this.owner = null;
|
|
40
|
+
this.repo = null;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.configured = true;
|
|
45
|
+
this.octokit = new Octokit({ auth: token });
|
|
46
|
+
this.owner = owner;
|
|
47
|
+
this.repo = repo;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if GitHub client is configured and ready
|
|
52
|
+
*
|
|
53
|
+
* @returns {boolean} True if configured, false otherwise
|
|
54
|
+
*/
|
|
55
|
+
isConfigured() {
|
|
56
|
+
return this.configured;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a new GitHub issue
|
|
61
|
+
*
|
|
62
|
+
* @param {string} title - Issue title
|
|
63
|
+
* @param {string} body - Issue body (markdown supported)
|
|
64
|
+
* @param {string[]} labels - Array of label names
|
|
65
|
+
* @returns {Promise<number|null>} Issue number if created, null on failure
|
|
66
|
+
*/
|
|
67
|
+
async createIssue(title, body, labels = []) {
|
|
68
|
+
if (!this.configured) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await this.octokit.issues.create({
|
|
74
|
+
owner: this.owner,
|
|
75
|
+
repo: this.repo,
|
|
76
|
+
title,
|
|
77
|
+
body,
|
|
78
|
+
labels
|
|
79
|
+
});
|
|
80
|
+
return response.data.number;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn('ā Failed to create GitHub issue:', error.message);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Update labels on an existing GitHub issue
|
|
89
|
+
*
|
|
90
|
+
* @param {number} issueNumber - Issue number
|
|
91
|
+
* @param {string[]} labels - Array of label names to set
|
|
92
|
+
* @returns {Promise<boolean>} True if updated, false on failure
|
|
93
|
+
*/
|
|
94
|
+
async updateIssueLabels(issueNumber, labels) {
|
|
95
|
+
if (!this.configured || !issueNumber) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await this.octokit.issues.update({
|
|
101
|
+
owner: this.owner,
|
|
102
|
+
repo: this.repo,
|
|
103
|
+
issue_number: issueNumber,
|
|
104
|
+
labels
|
|
105
|
+
});
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.warn('ā Failed to update issue labels:', error.message);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a comment to an existing GitHub issue
|
|
115
|
+
*
|
|
116
|
+
* @param {number} issueNumber - Issue number
|
|
117
|
+
* @param {string} comment - Comment text (markdown supported)
|
|
118
|
+
* @returns {Promise<boolean>} True if added, false on failure
|
|
119
|
+
*/
|
|
120
|
+
async addIssueComment(issueNumber, comment) {
|
|
121
|
+
if (!this.configured || !issueNumber) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await this.octokit.issues.createComment({
|
|
127
|
+
owner: this.owner,
|
|
128
|
+
repo: this.repo,
|
|
129
|
+
issue_number: issueNumber,
|
|
130
|
+
body: comment
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.warn('ā Failed to add issue comment:', error.message);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Close a GitHub issue with optional comment
|
|
141
|
+
*
|
|
142
|
+
* @param {number} issueNumber - Issue number
|
|
143
|
+
* @param {string|null} comment - Optional closing comment
|
|
144
|
+
* @returns {Promise<boolean>} True if closed, false on failure
|
|
145
|
+
*/
|
|
146
|
+
async closeIssue(issueNumber, comment = null) {
|
|
147
|
+
if (!this.configured || !issueNumber) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Add comment first if provided
|
|
153
|
+
if (comment) {
|
|
154
|
+
await this.addIssueComment(issueNumber, comment);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Close the issue
|
|
158
|
+
await this.octokit.issues.update({
|
|
159
|
+
owner: this.owner,
|
|
160
|
+
repo: this.repo,
|
|
161
|
+
issue_number: issueNumber,
|
|
162
|
+
state: 'closed'
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.warn('ā Failed to close issue:', error.message);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|