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.
@@ -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
+ }