agent-team-composer
Version:
Transform README files into GitHub project plans with AI-powered agent teams
277 lines (272 loc) • 8.84 kB
JavaScript
import { Octokit } from '@octokit/rest';
import { z } from 'zod';
// Input validation schemas
const IssueSchema = z.object({
title: z.string().min(1).max(256),
body: z.string().max(65536),
labels: z.array(z.string()).max(100),
assignees: z.array(z.string()).optional()
});
const PhaseSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string(),
roles: z.array(z.string()),
issues: z.array(z.object({
title: z.string(),
description: z.string(),
role: z.string()
})),
approved: z.boolean()
});
export class GitHubService {
octokit;
owner;
repo;
constructor(token) {
// Use GitHub token from environment or GitHub CLI config
this.octokit = new Octokit({
auth: token || process.env.GITHUB_TOKEN,
userAgent: 'agent-team-composer/1.0.0'
});
// These will be set when creating issues
this.owner = '';
this.repo = '';
}
/**
* Safely extract repository info from string
*/
parseRepository(repository) {
const parts = repository.split('/').filter(Boolean);
if (parts.length !== 2) {
throw new Error('Invalid repository format. Expected: owner/repo');
}
const [owner, repo] = parts;
// Validate repository name format
const repoNameRegex = /^[a-zA-Z0-9._-]+$/;
if (!repoNameRegex.test(owner) || !repoNameRegex.test(repo)) {
throw new Error('Invalid repository name format');
}
return { owner, repo };
}
/**
* Sanitize text for GitHub issues
*/
sanitizeText(text) {
return text
.replace(/[<>]/g, '') // Remove potential HTML
.trim()
.slice(0, 65536); // GitHub's max issue body length
}
/**
* Create GitHub issues from phases
*/
async createIssuesFromPhases(repository, phases) {
// Validate inputs
const { owner, repo } = this.parseRepository(repository);
this.owner = owner;
this.repo = repo;
// Validate phases
const validatedPhases = phases
.filter(p => p.approved)
.map(phase => {
const parsed = PhaseSchema.parse(phase);
return {
...parsed,
id: parsed.id,
title: parsed.title,
description: parsed.description,
roles: parsed.roles,
issues: parsed.issues,
approved: parsed.approved
};
});
const results = [];
try {
// Check repository access
await this.octokit.repos.get({ owner, repo });
}
catch (error) {
throw new Error(`Cannot access repository ${owner}/${repo}. Please check permissions.`);
}
// Create issues for each phase
for (const phase of validatedPhases) {
try {
// Create epic issue
const epicTitle = this.sanitizeText(`[Epic] ${phase.title}`);
const epicBody = this.createEpicBody(phase);
const epicResponse = await this.octokit.issues.create({
owner,
repo,
title: epicTitle,
body: epicBody,
labels: ['epic', `phase:${phase.id}`]
});
const epicNumber = epicResponse.data.number;
// Create individual issues
let issueCount = 0;
for (const issue of phase.issues) {
try {
const issueTitle = this.sanitizeText(issue.title);
const issueBody = this.createIssueBody(issue, epicNumber);
const labels = [
`phase:${phase.id}`,
`role:${this.sanitizeLabel(issue.role)}`
];
await this.octokit.issues.create({
owner,
repo,
title: issueTitle,
body: issueBody,
labels
});
issueCount++;
}
catch (error) {
console.error(`Failed to create issue: ${issue.title}`, error);
// Continue with other issues
}
}
results.push({
phase: phase.title,
epicNumber,
issueCount
});
}
catch (error) {
console.error(`Failed to create epic for phase: ${phase.title}`, error);
throw new Error(`Failed to create issues for phase ${phase.title}`);
}
}
return results;
}
/**
* Create epic body with proper formatting
*/
createEpicBody(phase) {
const rolesList = phase.roles.map(r => `- ${this.sanitizeText(r)}`).join('\n');
const tasksList = phase.issues.map(i => `- [ ] ${this.sanitizeText(i.title)}`).join('\n');
return this.sanitizeText(`
## ${phase.description}
### Roles
${rolesList}
### Tasks
${tasksList}
---
*This epic was generated by Agent Team Composer*
`.trim());
}
/**
* Create issue body with proper formatting
*/
createIssueBody(issue, epicNumber) {
return this.sanitizeText(`
${issue.description}
**Assigned Role:** ${issue.role}
**Epic:** #${epicNumber}
---
*This issue was generated by Agent Team Composer and is optimized for Claude Flow SPARC agents*
`.trim());
}
/**
* Sanitize label names
*/
sanitizeLabel(label) {
return label
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 50); // GitHub's max label length
}
/**
* Check GitHub authentication status
*/
async checkAuthentication() {
try {
const { data } = await this.octokit.users.getAuthenticated();
// Check if we have required scopes
const response = await this.octokit.request('GET /user');
const scopes = response.headers['x-oauth-scopes']?.split(', ') || [];
const hasRequiredScopes = scopes.includes('repo') || scopes.includes('public_repo');
if (!hasRequiredScopes) {
return {
authenticated: true,
user: data.login,
scopes,
error: 'Missing required GitHub scopes. Need "repo" or "public_repo" scope.'
};
}
return {
authenticated: true,
user: data.login,
scopes
};
}
catch (error) {
return {
authenticated: false,
error: 'Not authenticated with GitHub'
};
}
}
/**
* Get rate limit status
*/
async getRateLimit() {
const { data } = await this.octokit.rateLimit.get();
return {
limit: data.rate.limit,
remaining: data.rate.remaining,
reset: new Date(data.rate.reset * 1000)
};
}
/**
* Get user's repositories with write access
*/
async getUserRepositories() {
const repos = [];
let page = 1;
const perPage = 100;
while (true) {
const { data } = await this.octokit.repos.listForAuthenticatedUser({
page,
per_page: perPage,
sort: 'updated',
direction: 'desc'
});
if (data.length === 0)
break;
repos.push(...data);
if (data.length < perPage)
break;
page++;
}
return repos;
}
/**
* Get branches for a repository
*/
async getRepositoryBranches(repoFullName) {
const { owner, repo } = this.parseRepository(repoFullName);
const branches = [];
let page = 1;
const perPage = 100;
while (true) {
const { data } = await this.octokit.repos.listBranches({
owner,
repo,
page,
per_page: perPage
});
if (data.length === 0)
break;
branches.push(...data.map(branch => branch.name));
if (data.length < perPage)
break;
page++;
}
return branches;
}
}
//# sourceMappingURL=github-service.js.map