den-github-manager 1.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 +41 -0
- package/CONTRIBUTING.md +91 -0
- package/LICENSE +21 -0
- package/PRE_PUBLISH_CHECKLIST.md +165 -0
- package/README.md +307 -0
- package/bin/den.js +69 -0
- package/docs/backlog.md +78 -0
- package/package.json +59 -0
- package/scripts/pre-publish-test.sh +122 -0
- package/src/commands/analyze.js +35 -0
- package/src/commands/config.js +42 -0
- package/src/commands/init.js +224 -0
- package/src/commands/templates.js +71 -0
- package/src/core/analyzer.js +266 -0
- package/src/core/generator.js +199 -0
- package/src/core/github-client.js +246 -0
- package/src/core/template-manager.js +23 -0
- package/src/index.js +6 -0
- package/src/utils/config.js +57 -0
- package/src/utils/logger.js +49 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
export class Analyzer {
|
|
6
|
+
constructor(projectPath = process.cwd()) {
|
|
7
|
+
this.projectPath = projectPath;
|
|
8
|
+
this.logger = new Logger();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Analyze project and return comprehensive report
|
|
13
|
+
*/
|
|
14
|
+
async analyze() {
|
|
15
|
+
this.logger.info('🔍 Analyzing project structure...');
|
|
16
|
+
|
|
17
|
+
const analysis = {
|
|
18
|
+
projectPath: this.projectPath,
|
|
19
|
+
projectName: path.basename(this.projectPath),
|
|
20
|
+
hasGit: await this.checkGit(),
|
|
21
|
+
hasBacklog: await this.checkBacklog(),
|
|
22
|
+
hasREADME: await this.checkREADME(),
|
|
23
|
+
hasPackageJson: await this.checkPackageJson(),
|
|
24
|
+
hasDocumentation: await this.checkDocumentation(),
|
|
25
|
+
existingIssues: await this.checkExistingIssues(),
|
|
26
|
+
existingMilestones: await this.checkExistingMilestones(),
|
|
27
|
+
projectType: await this.detectProjectType(),
|
|
28
|
+
techStack: await this.detectTechStack(),
|
|
29
|
+
phase: await this.detectPhase(),
|
|
30
|
+
filesStructure: await this.analyzeStructure()
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return analysis;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async checkGit() {
|
|
37
|
+
try {
|
|
38
|
+
await fs.access(path.join(this.projectPath, '.git'));
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async checkBacklog() {
|
|
46
|
+
const possiblePaths = [
|
|
47
|
+
'docs/backlog.md',
|
|
48
|
+
'BACKLOG.md',
|
|
49
|
+
'docs/BACKLOG.md',
|
|
50
|
+
'documentation/backlog.md'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const p of possiblePaths) {
|
|
54
|
+
try {
|
|
55
|
+
const fullPath = path.join(this.projectPath, p);
|
|
56
|
+
await fs.access(fullPath);
|
|
57
|
+
return { exists: true, path: p };
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { exists: false, path: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async checkREADME() {
|
|
67
|
+
try {
|
|
68
|
+
const readmePath = path.join(this.projectPath, 'README.md');
|
|
69
|
+
const content = await fs.readFile(readmePath, 'utf-8');
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
exists: true,
|
|
73
|
+
hasRoadmap: /roadmap/i.test(content),
|
|
74
|
+
hasFeatures: /features/i.test(content),
|
|
75
|
+
hasTODO: /todo|to-do/i.test(content),
|
|
76
|
+
length: content.length
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return { exists: false };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async checkPackageJson() {
|
|
84
|
+
try {
|
|
85
|
+
const pkgPath = path.join(this.projectPath, 'package.json');
|
|
86
|
+
const content = await fs.readFile(pkgPath, 'utf-8');
|
|
87
|
+
const pkg = JSON.parse(content);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
exists: true,
|
|
91
|
+
name: pkg.name,
|
|
92
|
+
version: pkg.version,
|
|
93
|
+
scripts: Object.keys(pkg.scripts || {}),
|
|
94
|
+
dependencies: Object.keys(pkg.dependencies || {}).length
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
return { exists: false };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async checkDocumentation() {
|
|
102
|
+
const docPaths = ['docs', 'documentation', 'doc'];
|
|
103
|
+
|
|
104
|
+
for (const dir of docPaths) {
|
|
105
|
+
try {
|
|
106
|
+
const fullPath = path.join(this.projectPath, dir);
|
|
107
|
+
const stats = await fs.stat(fullPath);
|
|
108
|
+
if (stats.isDirectory()) {
|
|
109
|
+
const files = await fs.readdir(fullPath);
|
|
110
|
+
return {
|
|
111
|
+
exists: true,
|
|
112
|
+
path: dir,
|
|
113
|
+
files: files.filter(f => f.endsWith('.md')),
|
|
114
|
+
count: files.length
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { exists: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async checkExistingIssues() {
|
|
126
|
+
// This would require GitHub API integration
|
|
127
|
+
// For now, return placeholder
|
|
128
|
+
return {
|
|
129
|
+
checked: false,
|
|
130
|
+
count: 0,
|
|
131
|
+
message: 'GitHub API integration needed'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async checkExistingMilestones() {
|
|
136
|
+
// This would require GitHub API integration
|
|
137
|
+
return {
|
|
138
|
+
checked: false,
|
|
139
|
+
count: 0,
|
|
140
|
+
message: 'GitHub API integration needed'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async detectProjectType() {
|
|
145
|
+
const indicators = {
|
|
146
|
+
'package.json': 'nodejs',
|
|
147
|
+
'requirements.txt': 'python',
|
|
148
|
+
'Cargo.toml': 'rust',
|
|
149
|
+
'go.mod': 'go',
|
|
150
|
+
'pom.xml': 'java',
|
|
151
|
+
'Gemfile': 'ruby',
|
|
152
|
+
'composer.json': 'php'
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (const [file, type] of Object.entries(indicators)) {
|
|
156
|
+
try {
|
|
157
|
+
await fs.access(path.join(this.projectPath, file));
|
|
158
|
+
return type;
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return 'unknown';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async detectTechStack() {
|
|
168
|
+
const stack = [];
|
|
169
|
+
|
|
170
|
+
// Check package.json for JavaScript/TypeScript
|
|
171
|
+
try {
|
|
172
|
+
const pkgPath = path.join(this.projectPath, 'package.json');
|
|
173
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
174
|
+
|
|
175
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
176
|
+
|
|
177
|
+
if (deps.react) stack.push('React');
|
|
178
|
+
if (deps.next) stack.push('Next.js');
|
|
179
|
+
if (deps.vue) stack.push('Vue');
|
|
180
|
+
if (deps.express) stack.push('Express');
|
|
181
|
+
if (deps.typescript) stack.push('TypeScript');
|
|
182
|
+
} catch {}
|
|
183
|
+
|
|
184
|
+
return stack;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async detectPhase() {
|
|
188
|
+
// Detect project phase based on various indicators
|
|
189
|
+
const hasTests = await this.checkForTests();
|
|
190
|
+
const hasCI = await this.checkForCI();
|
|
191
|
+
const hasDocs = await this.checkDocumentation();
|
|
192
|
+
|
|
193
|
+
if (!hasTests && !hasCI && !hasDocs.exists) {
|
|
194
|
+
return 'new'; // Early stage
|
|
195
|
+
} else if (hasTests || hasDocs.exists) {
|
|
196
|
+
return 'development'; // Active development
|
|
197
|
+
} else if (hasTests && hasCI) {
|
|
198
|
+
return 'production'; // Mature project
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return 'unknown';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async checkForTests() {
|
|
205
|
+
const testDirs = ['test', 'tests', '__tests__', 'spec'];
|
|
206
|
+
|
|
207
|
+
for (const dir of testDirs) {
|
|
208
|
+
try {
|
|
209
|
+
await fs.access(path.join(this.projectPath, dir));
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async checkForCI() {
|
|
220
|
+
try {
|
|
221
|
+
await fs.access(path.join(this.projectPath, '.github/workflows'));
|
|
222
|
+
return true;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async analyzeStructure() {
|
|
229
|
+
try {
|
|
230
|
+
const files = await fs.readdir(this.projectPath);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
hasSource: files.includes('src') || files.includes('lib'),
|
|
234
|
+
hasTests: files.includes('test') || files.includes('tests'),
|
|
235
|
+
hasPublic: files.includes('public'),
|
|
236
|
+
hasAssets: files.includes('assets'),
|
|
237
|
+
totalFiles: files.length
|
|
238
|
+
};
|
|
239
|
+
} catch {
|
|
240
|
+
return { error: 'Could not read directory' };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate a summary report
|
|
246
|
+
*/
|
|
247
|
+
generateReport(analysis) {
|
|
248
|
+
this.logger.section('📊 Project Analysis Report');
|
|
249
|
+
|
|
250
|
+
this.logger.info(`Project: ${analysis.projectName}`);
|
|
251
|
+
this.logger.info(`Type: ${analysis.projectType}`);
|
|
252
|
+
this.logger.info(`Phase: ${analysis.phase}`);
|
|
253
|
+
|
|
254
|
+
if (analysis.techStack.length > 0) {
|
|
255
|
+
this.logger.info(`Tech Stack: ${analysis.techStack.join(', ')}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.logger.section('📁 Resources');
|
|
259
|
+
this.logger.status(analysis.hasGit, 'Git repository');
|
|
260
|
+
this.logger.status(analysis.hasBacklog.exists, `Backlog (${analysis.hasBacklog.path || 'not found'})`);
|
|
261
|
+
this.logger.status(analysis.hasREADME.exists, 'README.md');
|
|
262
|
+
this.logger.status(analysis.hasDocumentation.exists, `Documentation (${analysis.hasDocumentation.count || 0} files)`);
|
|
263
|
+
|
|
264
|
+
return analysis;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
|
|
3
|
+
export class Generator {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.logger = new Logger();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate backlog from analysis
|
|
10
|
+
*/
|
|
11
|
+
async generateBacklog(analysis, template = 'simple') {
|
|
12
|
+
this.logger.info('📝 Generating backlog...');
|
|
13
|
+
|
|
14
|
+
const backlog = {
|
|
15
|
+
milestones: this.generateMilestones(analysis, template),
|
|
16
|
+
epics: this.generateEpics(analysis, template),
|
|
17
|
+
stories: this.generateStories(analysis, template)
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return this.formatBacklog(backlog);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
generateMilestones(analysis, template) {
|
|
24
|
+
const templates = {
|
|
25
|
+
simple: [
|
|
26
|
+
{ id: 'M0', title: 'Foundation', desc: 'Setup and initial configuration', state: 'closed' },
|
|
27
|
+
{ id: 'M1', title: 'Core Features', desc: 'Main functionality', state: 'open' },
|
|
28
|
+
{ id: 'M2', title: 'Polish', desc: 'Testing and refinement', state: 'open' }
|
|
29
|
+
],
|
|
30
|
+
startup: [
|
|
31
|
+
{ id: 'M0', title: 'Foundation', desc: 'Repo, docs, environment', state: 'closed' },
|
|
32
|
+
{ id: 'M1', title: 'MVP Core', desc: 'Essential features for MVP', state: 'open' },
|
|
33
|
+
{ id: 'M2', title: 'User Testing', desc: 'Testing with real users', state: 'open' },
|
|
34
|
+
{ id: 'M3', title: 'Production Ready', desc: 'Launch preparation', state: 'open' }
|
|
35
|
+
],
|
|
36
|
+
agile: [
|
|
37
|
+
{ id: 'Sprint 1', title: 'Sprint 1', desc: 'Initial sprint - 2 weeks', state: 'open' },
|
|
38
|
+
{ id: 'Sprint 2', title: 'Sprint 2', desc: 'Second sprint - 2 weeks', state: 'open' },
|
|
39
|
+
{ id: 'Sprint 3', title: 'Sprint 3', desc: 'Third sprint - 2 weeks', state: 'open' }
|
|
40
|
+
],
|
|
41
|
+
enterprise: [
|
|
42
|
+
{ id: 'Phase 1', title: 'Discovery', desc: 'Requirements and planning', state: 'closed' },
|
|
43
|
+
{ id: 'Phase 2', title: 'Design', desc: 'Architecture and design', state: 'open' },
|
|
44
|
+
{ id: 'Phase 3', title: 'Development', desc: 'Implementation', state: 'open' },
|
|
45
|
+
{ id: 'Phase 4', title: 'Testing', desc: 'QA and testing', state: 'open' },
|
|
46
|
+
{ id: 'Phase 5', title: 'Deployment', desc: 'Production deployment', state: 'open' }
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return templates[template] || templates.simple;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
generateEpics(analysis, template) {
|
|
54
|
+
const baseEpics = [
|
|
55
|
+
{ id: 'E1', name: 'Setup & Configuration', milestone: 'M0' },
|
|
56
|
+
{ id: 'E2', name: 'Core Functionality', milestone: 'M1' },
|
|
57
|
+
{ id: 'E3', name: 'User Interface', milestone: 'M1' }
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Add tech-specific epics
|
|
61
|
+
if (analysis.techStack.includes('React')) {
|
|
62
|
+
baseEpics.push({ id: 'E4', name: 'React Components', milestone: 'M1' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (analysis.techStack.includes('Express')) {
|
|
66
|
+
baseEpics.push({ id: 'E5', name: 'API Endpoints', milestone: 'M1' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return baseEpics;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
generateStories(analysis, template) {
|
|
73
|
+
const stories = [];
|
|
74
|
+
|
|
75
|
+
// M0 stories (completed)
|
|
76
|
+
stories.push({
|
|
77
|
+
id: 'US-E1-01',
|
|
78
|
+
title: 'Setup repository',
|
|
79
|
+
priority: 'P0',
|
|
80
|
+
points: 3,
|
|
81
|
+
status: 'completed',
|
|
82
|
+
milestone: 'M0',
|
|
83
|
+
epic: 'E1'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
stories.push({
|
|
87
|
+
id: 'US-E1-02',
|
|
88
|
+
title: 'Configure development environment',
|
|
89
|
+
priority: 'P0',
|
|
90
|
+
points: 5,
|
|
91
|
+
status: 'completed',
|
|
92
|
+
milestone: 'M0',
|
|
93
|
+
epic: 'E1'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// M1 stories (pending)
|
|
97
|
+
stories.push({
|
|
98
|
+
id: 'US-E2-01',
|
|
99
|
+
title: 'Implement core logic',
|
|
100
|
+
priority: 'P0',
|
|
101
|
+
points: 8,
|
|
102
|
+
status: 'pending',
|
|
103
|
+
milestone: 'M1',
|
|
104
|
+
epic: 'E2'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
stories.push({
|
|
108
|
+
id: 'US-E3-01',
|
|
109
|
+
title: 'Create main UI components',
|
|
110
|
+
priority: 'P1',
|
|
111
|
+
points: 5,
|
|
112
|
+
status: 'pending',
|
|
113
|
+
milestone: 'M1',
|
|
114
|
+
epic: 'E3'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return stories;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
formatBacklog(backlog) {
|
|
121
|
+
let md = '# Product Backlog\n\n';
|
|
122
|
+
md += `Generated: ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
123
|
+
|
|
124
|
+
// Milestones
|
|
125
|
+
md += '## Milestones\n\n';
|
|
126
|
+
md += '| ID | Milestone | Objective | State |\n';
|
|
127
|
+
md += '|----|-----------|-----------|-------|\n';
|
|
128
|
+
|
|
129
|
+
for (const m of backlog.milestones) {
|
|
130
|
+
const icon = m.state === 'closed' ? '✅' : '⬜';
|
|
131
|
+
md += `| ${m.id} | ${m.title} | ${m.desc} | ${icon} ${m.state} |\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
md += '\n---\n\n';
|
|
135
|
+
|
|
136
|
+
// Epics
|
|
137
|
+
md += '## Epics\n\n';
|
|
138
|
+
md += '| ID | Epic | Description | Milestone |\n';
|
|
139
|
+
md += '|----|------|-------------|-----------||\n';
|
|
140
|
+
|
|
141
|
+
for (const e of backlog.epics) {
|
|
142
|
+
md += `| ${e.id} | ${e.name} | | ${e.milestone} |\n`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
md += '\n---\n\n';
|
|
146
|
+
|
|
147
|
+
// User Stories
|
|
148
|
+
md += '## User Stories\n\n';
|
|
149
|
+
|
|
150
|
+
for (const story of backlog.stories) {
|
|
151
|
+
const statusIcon = {
|
|
152
|
+
completed: '✅',
|
|
153
|
+
'in-progress': '🚧',
|
|
154
|
+
pending: '⬜'
|
|
155
|
+
}[story.status] || '⬜';
|
|
156
|
+
|
|
157
|
+
md += `### ${story.id}: ${story.title}\n\n`;
|
|
158
|
+
md += `**Priority**: ${story.priority} | **Points**: ${story.points} | **Status**: ${statusIcon} ${story.status}\n\n`;
|
|
159
|
+
md += `**Epic**: ${story.epic} | **Milestone**: ${story.milestone}\n\n`;
|
|
160
|
+
md += `**Criterios de Aceptación**:\n`;
|
|
161
|
+
md += `- [ ] Implement functionality\n`;
|
|
162
|
+
md += `- [ ] Add tests\n`;
|
|
163
|
+
md += `- [ ] Update documentation\n\n`;
|
|
164
|
+
md += '---\n\n';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return md;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate project config file
|
|
172
|
+
*/
|
|
173
|
+
generateConfig(analysis, options = {}) {
|
|
174
|
+
return {
|
|
175
|
+
version: '1.0.0',
|
|
176
|
+
projectName: analysis.projectName,
|
|
177
|
+
methodology: options.methodology || 'kanban',
|
|
178
|
+
backlogPath: 'docs/backlog.md',
|
|
179
|
+
templates: {
|
|
180
|
+
issue: '.github/ISSUE_TEMPLATE/user-story.md',
|
|
181
|
+
milestone: '.github/MILESTONE_TEMPLATE.md'
|
|
182
|
+
},
|
|
183
|
+
labels: {
|
|
184
|
+
priority: ['P0:Critical', 'P1:High', 'P2:Medium'],
|
|
185
|
+
epics: analysis.techStack.map((tech, i) => `Epic:E${i + 1}-${tech}`),
|
|
186
|
+
storyPoints: ['story-points:3', 'story-points:5', 'story-points:8'],
|
|
187
|
+
status: ['status:in-progress', 'status:completed']
|
|
188
|
+
},
|
|
189
|
+
milestones: {
|
|
190
|
+
prefix: options.milestonePrefix || 'M',
|
|
191
|
+
duration: options.duration || 'flexible'
|
|
192
|
+
},
|
|
193
|
+
project: {
|
|
194
|
+
name: `${analysis.projectName} Development Board`,
|
|
195
|
+
views: ['status', 'milestone', 'epic', 'priority']
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Octokit } from '@octokit/rest';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
export class GitHubClient {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.logger = new Logger();
|
|
11
|
+
this.octokit = null;
|
|
12
|
+
this.repo = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize GitHub client
|
|
17
|
+
*/
|
|
18
|
+
async init() {
|
|
19
|
+
try {
|
|
20
|
+
// Try to get token from gh CLI
|
|
21
|
+
const { stdout } = await execAsync('gh auth token');
|
|
22
|
+
const token = stdout.trim();
|
|
23
|
+
|
|
24
|
+
this.octokit = new Octokit({ auth: token });
|
|
25
|
+
this.repo = await this.detectRepo();
|
|
26
|
+
|
|
27
|
+
this.logger.success('GitHub client initialized');
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
this.logger.error('Failed to initialize GitHub client');
|
|
31
|
+
this.logger.error('Make sure gh CLI is installed and authenticated');
|
|
32
|
+
this.logger.info('Run: gh auth login');
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect current repository
|
|
39
|
+
*/
|
|
40
|
+
async detectRepo() {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execAsync('git remote get-url origin');
|
|
43
|
+
const match = stdout.match(/github\.com[:/](.+?)\/(.+?)\.git/);
|
|
44
|
+
|
|
45
|
+
if (match) {
|
|
46
|
+
return {
|
|
47
|
+
owner: match[1],
|
|
48
|
+
repo: match[2]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
this.logger.warn('Could not detect GitHub repository');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create milestones
|
|
60
|
+
*/
|
|
61
|
+
async createMilestones(milestones) {
|
|
62
|
+
if (!this.octokit || !this.repo) {
|
|
63
|
+
throw new Error('GitHub client not initialized');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.logger.info(`Creating ${milestones.length} milestones...`);
|
|
67
|
+
const created = [];
|
|
68
|
+
|
|
69
|
+
for (const milestone of milestones) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.octokit.issues.createMilestone({
|
|
72
|
+
owner: this.repo.owner,
|
|
73
|
+
repo: this.repo.repo,
|
|
74
|
+
title: milestone.title,
|
|
75
|
+
description: milestone.desc,
|
|
76
|
+
state: milestone.state || 'open'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
created.push(result.data);
|
|
80
|
+
this.logger.success(`✓ ${milestone.title}`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.status === 422) {
|
|
83
|
+
this.logger.warn(`⚠ Milestone already exists: ${milestone.title}`);
|
|
84
|
+
} else {
|
|
85
|
+
this.logger.error(`✗ Failed to create: ${milestone.title}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return created;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create labels
|
|
95
|
+
*/
|
|
96
|
+
async createLabels(labels) {
|
|
97
|
+
if (!this.octokit || !this.repo) {
|
|
98
|
+
throw new Error('GitHub client not initialized');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.logger.info(`Creating labels...`);
|
|
102
|
+
const created = [];
|
|
103
|
+
|
|
104
|
+
const labelConfigs = [
|
|
105
|
+
{ name: 'P0:Critical', color: 'd73a4a', description: 'Critical priority' },
|
|
106
|
+
{ name: 'P1:High', color: 'ff9800', description: 'High priority' },
|
|
107
|
+
{ name: 'P2:Medium', color: 'fbca04', description: 'Medium priority' },
|
|
108
|
+
{ name: 'status:in-progress', color: '0075ca', description: 'In progress' },
|
|
109
|
+
{ name: 'status:completed', color: '0e8a16', description: 'Completed' },
|
|
110
|
+
{ name: 'story-points:3', color: 'c5def5', description: '3 story points' },
|
|
111
|
+
{ name: 'story-points:5', color: 'c5def5', description: '5 story points' },
|
|
112
|
+
{ name: 'story-points:8', color: 'c5def5', description: '8 story points' },
|
|
113
|
+
...labels.map(l => ({
|
|
114
|
+
name: l.name || l,
|
|
115
|
+
color: l.color || 'ededed',
|
|
116
|
+
description: l.description || ''
|
|
117
|
+
}))
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const label of labelConfigs) {
|
|
121
|
+
try {
|
|
122
|
+
const result = await this.octokit.issues.createLabel({
|
|
123
|
+
owner: this.repo.owner,
|
|
124
|
+
repo: this.repo.repo,
|
|
125
|
+
name: label.name,
|
|
126
|
+
color: label.color,
|
|
127
|
+
description: label.description
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
created.push(result.data);
|
|
131
|
+
this.logger.success(`✓ ${label.name}`);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.status === 422) {
|
|
134
|
+
// Label already exists, skip
|
|
135
|
+
} else {
|
|
136
|
+
this.logger.warn(`⚠ ${label.name}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return created;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create issues
|
|
146
|
+
*/
|
|
147
|
+
async createIssues(stories, milestones) {
|
|
148
|
+
if (!this.octokit || !this.repo) {
|
|
149
|
+
throw new Error('GitHub client not initialized');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.logger.info(`Creating ${stories.length} issues...`);
|
|
153
|
+
const created = [];
|
|
154
|
+
|
|
155
|
+
// Get milestone numbers
|
|
156
|
+
const { data: existingMilestones } = await this.octokit.issues.listMilestones({
|
|
157
|
+
owner: this.repo.owner,
|
|
158
|
+
repo: this.repo.repo,
|
|
159
|
+
state: 'all'
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const milestoneMap = new Map(
|
|
163
|
+
existingMilestones.map(m => [m.title, m.number])
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
for (const story of stories) {
|
|
167
|
+
try {
|
|
168
|
+
const labels = [story.priority, `story-points:${story.points}`];
|
|
169
|
+
|
|
170
|
+
if (story.epic) labels.push(story.epic);
|
|
171
|
+
if (story.status) labels.push(`status:${story.status}`);
|
|
172
|
+
|
|
173
|
+
const milestoneNumber = milestoneMap.get(story.milestone);
|
|
174
|
+
|
|
175
|
+
const issue = await this.octokit.issues.create({
|
|
176
|
+
owner: this.repo.owner,
|
|
177
|
+
repo: this.repo.repo,
|
|
178
|
+
title: `${story.id}: ${story.title}`,
|
|
179
|
+
body: this.formatIssueBody(story),
|
|
180
|
+
labels: labels.filter(Boolean),
|
|
181
|
+
milestone: milestoneNumber
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Close if completed
|
|
185
|
+
if (story.status === 'completed') {
|
|
186
|
+
await this.octokit.issues.update({
|
|
187
|
+
owner: this.repo.owner,
|
|
188
|
+
repo: this.repo.repo,
|
|
189
|
+
issue_number: issue.data.number,
|
|
190
|
+
state: 'closed'
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
created.push(issue.data);
|
|
195
|
+
this.logger.success(`✓ #${issue.data.number} ${story.title}`);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.logger.error(`✗ Failed: ${story.title}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return created;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
formatIssueBody(story) {
|
|
205
|
+
let body = `## User Story\n\n`;
|
|
206
|
+
body += `**Priority:** ${story.priority}\n`;
|
|
207
|
+
body += `**Story Points:** ${story.points}\n`;
|
|
208
|
+
body += `**Epic:** ${story.epic}\n`;
|
|
209
|
+
body += `**Milestone:** ${story.milestone}\n\n`;
|
|
210
|
+
body += `## Acceptance Criteria\n\n`;
|
|
211
|
+
body += `- [ ] Implement core functionality\n`;
|
|
212
|
+
body += `- [ ] Add unit tests\n`;
|
|
213
|
+
body += `- [ ] Update documentation\n`;
|
|
214
|
+
body += `- [ ] Code review completed\n\n`;
|
|
215
|
+
body += `---\n`;
|
|
216
|
+
body += `*Created by den-github-manager*`;
|
|
217
|
+
|
|
218
|
+
return body;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get repository info
|
|
223
|
+
*/
|
|
224
|
+
async getRepoInfo() {
|
|
225
|
+
if (!this.octokit || !this.repo) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const { data } = await this.octokit.repos.get({
|
|
231
|
+
owner: this.repo.owner,
|
|
232
|
+
repo: this.repo.repo
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
name: data.name,
|
|
237
|
+
fullName: data.full_name,
|
|
238
|
+
description: data.description,
|
|
239
|
+
url: data.html_url,
|
|
240
|
+
private: data.private
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|