mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-30 09:27:34 +08:00
- Replace mock data with real metrics from .claude-flow/metrics/ files - Fix MCP Server detection to properly show running status - Make agent spawn and task creation fully functional with file persistence - Update binary wrapper to prioritize local source over npm packages - Bump version to 2.0.0-alpha.111 Changes: - Created MetricsReader utility to read actual metrics files - Updated status.js to display real system metrics, not hardcoded values - Fixed agent.js to create real agent files in .claude-flow/agents/ - Fixed task.js to maintain actual task queue in .claude-flow/tasks/ - Modified ./claude-flow binary to use local source directly - Broadened MCP detection pattern to catch all MCP variants All status commands now show live, accurate data: - Real memory usage, CPU load, and disk stats - Actual agent counts and statuses from persisted files - Live task queue with real queued tasks - Correct MCP server running status - Accurate session and performance metrics 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
625 lines
17 KiB
JavaScript
625 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* GitHub API Integration Module
|
|
* Provides authentication, rate limiting, and API wrappers for GitHub workflow commands
|
|
*
|
|
* Enhanced with GitHub CLI Safety Wrapper for secure command execution
|
|
*/
|
|
|
|
import { printSuccess, printError, printWarning, printInfo } from '../utils.js';
|
|
import { githubCli, GitHubCliSafe } from '../../utils/github-cli-safety-wrapper.js';
|
|
|
|
// GitHub API Configuration
|
|
const GITHUB_API_BASE = 'https://api.github.com';
|
|
const GITHUB_RATE_LIMIT = 5000; // API calls per hour
|
|
const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
|
|
|
|
class GitHubAPIClient {
|
|
constructor(token = null) {
|
|
this.token = token || process.env.GITHUB_TOKEN;
|
|
this.rateLimitRemaining = GITHUB_RATE_LIMIT;
|
|
this.rateLimitResetTime = null;
|
|
this.lastRequestTime = 0;
|
|
this.requestQueue = [];
|
|
this.isProcessingQueue = false;
|
|
|
|
// Initialize GitHub CLI safety wrapper
|
|
this.cliSafe = new GitHubCliSafe({
|
|
timeout: 60000, // 1 minute timeout for CLI operations
|
|
maxRetries: 3,
|
|
enableRateLimit: true,
|
|
enableLogging: false // Can be enabled for debugging
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Authentication Methods
|
|
*/
|
|
async authenticate(token = null) {
|
|
if (token) {
|
|
this.token = token;
|
|
}
|
|
|
|
if (!this.token) {
|
|
printError('GitHub token not found. Set GITHUB_TOKEN environment variable or provide token.');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await this.request('/user');
|
|
if (response.success) {
|
|
printSuccess(`Authenticated as ${response.data.login}`);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
printError(`Authentication failed: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rate Limiting Management
|
|
*/
|
|
async checkRateLimit() {
|
|
if (this.rateLimitRemaining <= 1) {
|
|
const resetTime = new Date(this.rateLimitResetTime);
|
|
const now = new Date();
|
|
const waitTime = resetTime.getTime() - now.getTime();
|
|
|
|
if (waitTime > 0) {
|
|
printWarning(`Rate limit exceeded. Waiting ${Math.ceil(waitTime / 1000)}s...`);
|
|
await this.sleep(waitTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateRateLimitInfo(headers) {
|
|
this.rateLimitRemaining = parseInt(headers['x-ratelimit-remaining'] || '0');
|
|
this.rateLimitResetTime = new Date((parseInt(headers['x-ratelimit-reset']) || 0) * 1000);
|
|
}
|
|
|
|
/**
|
|
* Core API Request Method
|
|
*/
|
|
async request(endpoint, options = {}) {
|
|
await this.checkRateLimit();
|
|
|
|
const url = endpoint.startsWith('http') ? endpoint : `${GITHUB_API_BASE}${endpoint}`;
|
|
const headers = {
|
|
Authorization: `token ${this.token}`,
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'User-Agent': 'Claude-Flow-GitHub-Integration',
|
|
...options.headers,
|
|
};
|
|
|
|
const requestOptions = {
|
|
method: options.method || 'GET',
|
|
headers,
|
|
...options,
|
|
};
|
|
|
|
if (options.body) {
|
|
requestOptions.body = JSON.stringify(options.body);
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, requestOptions);
|
|
this.updateRateLimitInfo(response.headers);
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API error: ${data.message || response.statusText}`);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data,
|
|
headers: response.headers,
|
|
status: response.status,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
status: error.status || 500,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Repository Operations
|
|
*/
|
|
async getRepository(owner, repo) {
|
|
return await this.request(`/repos/${owner}/${repo}`);
|
|
}
|
|
|
|
async listRepositories(options = {}) {
|
|
const params = new URLSearchParams({
|
|
sort: options.sort || 'updated',
|
|
direction: options.direction || 'desc',
|
|
per_page: options.perPage || 30,
|
|
page: options.page || 1,
|
|
});
|
|
|
|
return await this.request(`/user/repos?${params}`);
|
|
}
|
|
|
|
async createRepository(repoData) {
|
|
return await this.request('/user/repos', {
|
|
method: 'POST',
|
|
body: repoData,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pull Request Operations
|
|
*/
|
|
async listPullRequests(owner, repo, options = {}) {
|
|
const params = new URLSearchParams({
|
|
state: options.state || 'open',
|
|
sort: options.sort || 'created',
|
|
direction: options.direction || 'desc',
|
|
per_page: options.perPage || 30,
|
|
page: options.page || 1,
|
|
});
|
|
|
|
return await this.request(`/repos/${owner}/${repo}/pulls?${params}`);
|
|
}
|
|
|
|
async createPullRequest(owner, repo, prData) {
|
|
return await this.request(`/repos/${owner}/${repo}/pulls`, {
|
|
method: 'POST',
|
|
body: prData,
|
|
});
|
|
}
|
|
|
|
async updatePullRequest(owner, repo, prNumber, prData) {
|
|
return await this.request(`/repos/${owner}/${repo}/pulls/${prNumber}`, {
|
|
method: 'PATCH',
|
|
body: prData,
|
|
});
|
|
}
|
|
|
|
async mergePullRequest(owner, repo, prNumber, mergeData) {
|
|
return await this.request(`/repos/${owner}/${repo}/pulls/${prNumber}/merge`, {
|
|
method: 'PUT',
|
|
body: mergeData,
|
|
});
|
|
}
|
|
|
|
async requestPullRequestReview(owner, repo, prNumber, reviewData) {
|
|
return await this.request(`/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`, {
|
|
method: 'POST',
|
|
body: reviewData,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Issue Operations
|
|
*/
|
|
async listIssues(owner, repo, options = {}) {
|
|
const params = new URLSearchParams({
|
|
state: options.state || 'open',
|
|
sort: options.sort || 'created',
|
|
direction: options.direction || 'desc',
|
|
per_page: options.perPage || 30,
|
|
page: options.page || 1,
|
|
});
|
|
|
|
if (options.labels) {
|
|
params.append('labels', options.labels);
|
|
}
|
|
|
|
return await this.request(`/repos/${owner}/${repo}/issues?${params}`);
|
|
}
|
|
|
|
async createIssue(owner, repo, issueData) {
|
|
return await this.request(`/repos/${owner}/${repo}/issues`, {
|
|
method: 'POST',
|
|
body: issueData,
|
|
});
|
|
}
|
|
|
|
async updateIssue(owner, repo, issueNumber, issueData) {
|
|
return await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}`, {
|
|
method: 'PATCH',
|
|
body: issueData,
|
|
});
|
|
}
|
|
|
|
async addIssueLabels(owner, repo, issueNumber, labels) {
|
|
return await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
|
|
method: 'POST',
|
|
body: { labels },
|
|
});
|
|
}
|
|
|
|
async assignIssue(owner, repo, issueNumber, assignees) {
|
|
return await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/assignees`, {
|
|
method: 'POST',
|
|
body: { assignees },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Release Operations
|
|
*/
|
|
async listReleases(owner, repo, options = {}) {
|
|
const params = new URLSearchParams({
|
|
per_page: options.perPage || 30,
|
|
page: options.page || 1,
|
|
});
|
|
|
|
return await this.request(`/repos/${owner}/${repo}/releases?${params}`);
|
|
}
|
|
|
|
async createRelease(owner, repo, releaseData) {
|
|
return await this.request(`/repos/${owner}/${repo}/releases`, {
|
|
method: 'POST',
|
|
body: releaseData,
|
|
});
|
|
}
|
|
|
|
async updateRelease(owner, repo, releaseId, releaseData) {
|
|
return await this.request(`/repos/${owner}/${repo}/releases/${releaseId}`, {
|
|
method: 'PATCH',
|
|
body: releaseData,
|
|
});
|
|
}
|
|
|
|
async deleteRelease(owner, repo, releaseId) {
|
|
return await this.request(`/repos/${owner}/${repo}/releases/${releaseId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Workflow Operations
|
|
*/
|
|
async listWorkflows(owner, repo) {
|
|
return await this.request(`/repos/${owner}/${repo}/actions/workflows`);
|
|
}
|
|
|
|
async triggerWorkflow(owner, repo, workflowId, ref = 'main', inputs = {}) {
|
|
return await this.request(
|
|
`/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`,
|
|
{
|
|
method: 'POST',
|
|
body: { ref, inputs },
|
|
},
|
|
);
|
|
}
|
|
|
|
async listWorkflowRuns(owner, repo, options = {}) {
|
|
const params = new URLSearchParams({
|
|
per_page: options.perPage || 30,
|
|
page: options.page || 1,
|
|
});
|
|
|
|
if (options.status) {
|
|
params.append('status', options.status);
|
|
}
|
|
|
|
return await this.request(`/repos/${owner}/${repo}/actions/runs?${params}`);
|
|
}
|
|
|
|
/**
|
|
* Branch Operations
|
|
*/
|
|
async listBranches(owner, repo) {
|
|
return await this.request(`/repos/${owner}/${repo}/branches`);
|
|
}
|
|
|
|
async createBranch(owner, repo, branchName, sha) {
|
|
return await this.request(`/repos/${owner}/${repo}/git/refs`, {
|
|
method: 'POST',
|
|
body: {
|
|
ref: `refs/heads/${branchName}`,
|
|
sha,
|
|
},
|
|
});
|
|
}
|
|
|
|
async getBranchProtection(owner, repo, branch) {
|
|
return await this.request(`/repos/${owner}/${repo}/branches/${branch}/protection`);
|
|
}
|
|
|
|
async updateBranchProtection(owner, repo, branch, protection) {
|
|
return await this.request(`/repos/${owner}/${repo}/branches/${branch}/protection`, {
|
|
method: 'PUT',
|
|
body: protection,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Webhook Operations
|
|
*/
|
|
async listWebhooks(owner, repo) {
|
|
return await this.request(`/repos/${owner}/${repo}/hooks`);
|
|
}
|
|
|
|
async createWebhook(owner, repo, webhookData) {
|
|
return await this.request(`/repos/${owner}/${repo}/hooks`, {
|
|
method: 'POST',
|
|
body: webhookData,
|
|
});
|
|
}
|
|
|
|
async updateWebhook(owner, repo, hookId, webhookData) {
|
|
return await this.request(`/repos/${owner}/${repo}/hooks/${hookId}`, {
|
|
method: 'PATCH',
|
|
body: webhookData,
|
|
});
|
|
}
|
|
|
|
async deleteWebhook(owner, repo, hookId) {
|
|
return await this.request(`/repos/${owner}/${repo}/hooks/${hookId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Event Processing
|
|
*/
|
|
async processWebhookEvent(event, signature, payload) {
|
|
if (!this.verifyWebhookSignature(signature, payload)) {
|
|
throw new Error('Invalid webhook signature');
|
|
}
|
|
|
|
const eventData = JSON.parse(payload);
|
|
|
|
switch (event) {
|
|
case 'push':
|
|
return this.handlePushEvent(eventData);
|
|
case 'pull_request':
|
|
return this.handlePullRequestEvent(eventData);
|
|
case 'issues':
|
|
return this.handleIssuesEvent(eventData);
|
|
case 'release':
|
|
return this.handleReleaseEvent(eventData);
|
|
case 'workflow_run':
|
|
return this.handleWorkflowRunEvent(eventData);
|
|
default:
|
|
printInfo(`Unhandled event type: ${event}`);
|
|
return { handled: false, event };
|
|
}
|
|
}
|
|
|
|
verifyWebhookSignature(signature, payload) {
|
|
if (!GITHUB_WEBHOOK_SECRET) {
|
|
printWarning('GITHUB_WEBHOOK_SECRET not set. Skipping signature verification.');
|
|
return true;
|
|
}
|
|
|
|
const crypto = require('crypto');
|
|
const hmac = crypto.createHmac('sha256', GITHUB_WEBHOOK_SECRET);
|
|
hmac.update(payload);
|
|
const expectedSignature = `sha256=${hmac.digest('hex')}`;
|
|
|
|
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
|
|
}
|
|
|
|
/**
|
|
* Event Handlers
|
|
*/
|
|
async handlePushEvent(eventData) {
|
|
printInfo(`Push event: ${eventData.commits.length} commits to ${eventData.ref}`);
|
|
return { handled: true, event: 'push', data: eventData };
|
|
}
|
|
|
|
async handlePullRequestEvent(eventData) {
|
|
const action = eventData.action;
|
|
const pr = eventData.pull_request;
|
|
printInfo(`Pull request ${action}: #${pr.number} - ${pr.title}`);
|
|
return { handled: true, event: 'pull_request', action, data: eventData };
|
|
}
|
|
|
|
async handleIssuesEvent(eventData) {
|
|
const action = eventData.action;
|
|
const issue = eventData.issue;
|
|
printInfo(`Issue ${action}: #${issue.number} - ${issue.title}`);
|
|
return { handled: true, event: 'issues', action, data: eventData };
|
|
}
|
|
|
|
async handleReleaseEvent(eventData) {
|
|
const action = eventData.action;
|
|
const release = eventData.release;
|
|
printInfo(`Release ${action}: ${release.tag_name} - ${release.name}`);
|
|
return { handled: true, event: 'release', action, data: eventData };
|
|
}
|
|
|
|
async handleWorkflowRunEvent(eventData) {
|
|
const action = eventData.action;
|
|
const workflowRun = eventData.workflow_run;
|
|
printInfo(`Workflow run ${action}: ${workflowRun.name} - ${workflowRun.conclusion}`);
|
|
return { handled: true, event: 'workflow_run', action, data: eventData };
|
|
}
|
|
|
|
/**
|
|
* Utility Methods
|
|
*/
|
|
async sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
parseRepository(repoString) {
|
|
const match = repoString.match(/^([^/]+)\/([^/]+)$/);
|
|
if (!match) {
|
|
throw new Error('Invalid repository format. Use: owner/repo');
|
|
}
|
|
return { owner: match[1], repo: match[2] };
|
|
}
|
|
|
|
formatDate(dateString) {
|
|
return new Date(dateString).toLocaleString();
|
|
}
|
|
|
|
formatFileSize(bytes) {
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let size = bytes;
|
|
let unitIndex = 0;
|
|
|
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
size /= 1024;
|
|
unitIndex++;
|
|
}
|
|
|
|
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
}
|
|
|
|
/**
|
|
* Safe GitHub CLI Methods
|
|
* These methods use the GitHubCliSafe wrapper for secure command execution
|
|
*/
|
|
|
|
/**
|
|
* Create issue using GitHub CLI (safe alternative to createIssue API method)
|
|
* @param {Object} issueData - Issue data
|
|
* @returns {Promise<Object>} - CLI execution result
|
|
*/
|
|
async createIssueCLI(issueData) {
|
|
try {
|
|
const result = await this.cliSafe.createIssue({
|
|
title: issueData.title,
|
|
body: issueData.body,
|
|
labels: issueData.labels || [],
|
|
assignees: issueData.assignees || []
|
|
});
|
|
|
|
printSuccess(`Issue created via CLI: ${issueData.title}`);
|
|
return { success: true, data: result };
|
|
} catch (error) {
|
|
printError(`Failed to create issue via CLI: ${error.message}`);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create PR using GitHub CLI (safe alternative to createPullRequest API method)
|
|
* @param {Object} prData - PR data
|
|
* @returns {Promise<Object>} - CLI execution result
|
|
*/
|
|
async createPullRequestCLI(prData) {
|
|
try {
|
|
const result = await this.cliSafe.createPR({
|
|
title: prData.title,
|
|
body: prData.body,
|
|
base: prData.base || 'main',
|
|
head: prData.head,
|
|
draft: prData.draft || false
|
|
});
|
|
|
|
printSuccess(`PR created via CLI: ${prData.title}`);
|
|
return { success: true, data: result };
|
|
} catch (error) {
|
|
printError(`Failed to create PR via CLI: ${error.message}`);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add issue comment using GitHub CLI
|
|
* @param {number} issueNumber - Issue number
|
|
* @param {string} body - Comment body
|
|
* @returns {Promise<Object>} - CLI execution result
|
|
*/
|
|
async addIssueCommentCLI(issueNumber, body) {
|
|
try {
|
|
const result = await this.cliSafe.addIssueComment(issueNumber, body);
|
|
printSuccess(`Comment added to issue #${issueNumber}`);
|
|
return { success: true, data: result };
|
|
} catch (error) {
|
|
printError(`Failed to add comment to issue #${issueNumber}: ${error.message}`);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add PR comment using GitHub CLI
|
|
* @param {number} prNumber - PR number
|
|
* @param {string} body - Comment body
|
|
* @returns {Promise<Object>} - CLI execution result
|
|
*/
|
|
async addPRCommentCLI(prNumber, body) {
|
|
try {
|
|
const result = await this.cliSafe.addPRComment(prNumber, body);
|
|
printSuccess(`Comment added to PR #${prNumber}`);
|
|
return { success: true, data: result };
|
|
} catch (error) {
|
|
printError(`Failed to add comment to PR #${prNumber}: ${error.message}`);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create release using GitHub CLI
|
|
* @param {Object} releaseData - Release data
|
|
* @returns {Promise<Object>} - CLI execution result
|
|
*/
|
|
async createReleaseCLI(releaseData) {
|
|
try {
|
|
const result = await this.cliSafe.createRelease({
|
|
tag: releaseData.tag_name,
|
|
title: releaseData.name,
|
|
body: releaseData.body,
|
|
prerelease: releaseData.prerelease || false,
|
|
draft: releaseData.draft || false
|
|
});
|
|
|
|
printSuccess(`Release created via CLI: ${releaseData.tag_name}`);
|
|
return { success: true, data: result };
|
|
} catch (error) {
|
|
printError(`Failed to create release via CLI: ${error.message}`);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check GitHub CLI authentication and availability
|
|
* @returns {Promise<boolean>} - True if CLI is ready to use
|
|
*/
|
|
async checkCLIStatus() {
|
|
try {
|
|
const isAvailable = await this.cliSafe.checkGitHubCli();
|
|
if (!isAvailable) {
|
|
printWarning('GitHub CLI is not installed or not in PATH');
|
|
return false;
|
|
}
|
|
|
|
const isAuthenticated = await this.cliSafe.checkAuthentication();
|
|
if (!isAuthenticated) {
|
|
printWarning('GitHub CLI is not authenticated. Run: gh auth login');
|
|
return false;
|
|
}
|
|
|
|
printSuccess('GitHub CLI is ready for use');
|
|
return true;
|
|
} catch (error) {
|
|
printError(`GitHub CLI status check failed: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get GitHub CLI wrapper statistics
|
|
* @returns {Object} - Usage statistics
|
|
*/
|
|
getCLIStats() {
|
|
return this.cliSafe.getStats();
|
|
}
|
|
|
|
/**
|
|
* Cleanup CLI resources (call before shutdown)
|
|
*/
|
|
async cleanupCLI() {
|
|
await this.cliSafe.cleanup();
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const githubAPI = new GitHubAPIClient();
|
|
export default GitHubAPIClient;
|