claude-autopm 2.6.0 → 2.7.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/bin/autopm.js +2 -0
- package/lib/cli/commands/context.js +477 -0
- package/lib/cli/commands/pm.js +300 -1
- package/lib/services/ContextService.js +595 -0
- package/lib/services/UtilityService.js +847 -0
- package/package.json +1 -1
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextService - Context Management Service
|
|
3
|
+
*
|
|
4
|
+
* Pure service layer for context operations following ClaudeAutoPM patterns.
|
|
5
|
+
* Manages project context files for AI-assisted development.
|
|
6
|
+
*
|
|
7
|
+
* Provides comprehensive context lifecycle management:
|
|
8
|
+
*
|
|
9
|
+
* 1. Context Creation (1 method):
|
|
10
|
+
* - createContext: Create new context from template
|
|
11
|
+
*
|
|
12
|
+
* 2. Context Priming (1 method):
|
|
13
|
+
* - primeContext: Generate comprehensive project snapshot
|
|
14
|
+
*
|
|
15
|
+
* 3. Context Updates (1 method):
|
|
16
|
+
* - updateContext: Update existing context (append/replace)
|
|
17
|
+
*
|
|
18
|
+
* 4. Context Reading (2 methods):
|
|
19
|
+
* - getContext: Read context file with metadata
|
|
20
|
+
* - listContexts: List all contexts grouped by type
|
|
21
|
+
*
|
|
22
|
+
* 5. Context Validation (1 method):
|
|
23
|
+
* - validateContext: Validate structure and required fields
|
|
24
|
+
*
|
|
25
|
+
* 6. Context Operations (2 methods):
|
|
26
|
+
* - mergeContexts: Merge multiple contexts
|
|
27
|
+
* - analyzeContextUsage: Analyze size, age, and generate recommendations
|
|
28
|
+
*
|
|
29
|
+
* Documentation Queries:
|
|
30
|
+
* - mcp://context7/project-management/context-management - Context management patterns
|
|
31
|
+
* - mcp://context7/documentation/context-files - Context file best practices
|
|
32
|
+
* - mcp://context7/ai/context-optimization - Context optimization strategies
|
|
33
|
+
* - mcp://context7/project-management/project-brief - Project brief formats
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('fs-extra');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
|
|
39
|
+
class ContextService {
|
|
40
|
+
/**
|
|
41
|
+
* Create a new ContextService instance
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} options - Configuration options
|
|
44
|
+
* @param {string} [options.templatesPath] - Path to templates directory
|
|
45
|
+
* @param {string} [options.contextPath] - Path to contexts directory
|
|
46
|
+
* @param {string} [options.epicsPath] - Path to epics directory
|
|
47
|
+
* @param {string} [options.issuesPath] - Path to issues directory
|
|
48
|
+
*/
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.templatesPath = options.templatesPath ||
|
|
51
|
+
path.join(process.cwd(), '.claude/templates/context-templates');
|
|
52
|
+
this.contextPath = options.contextPath ||
|
|
53
|
+
path.join(process.cwd(), '.claude/context');
|
|
54
|
+
this.epicsPath = options.epicsPath ||
|
|
55
|
+
path.join(process.cwd(), '.claude/epics');
|
|
56
|
+
this.issuesPath = options.issuesPath ||
|
|
57
|
+
path.join(process.cwd(), '.claude/issues');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ==========================================
|
|
61
|
+
// 1. CONTEXT CREATION (1 METHOD)
|
|
62
|
+
// ==========================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create new context file from template
|
|
66
|
+
*
|
|
67
|
+
* @param {string} type - Context type: project-brief, progress, tech-context, project-structure
|
|
68
|
+
* @param {object} options - Creation options (name, template, data)
|
|
69
|
+
* @returns {Promise<{path, content, created, type}>}
|
|
70
|
+
* @throws {Error} If template not found
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* await createContext('project-brief', {
|
|
74
|
+
* name: 'My Project',
|
|
75
|
+
* description: 'Project description'
|
|
76
|
+
* })
|
|
77
|
+
*/
|
|
78
|
+
async createContext(type, options = {}) {
|
|
79
|
+
// Load template
|
|
80
|
+
const templatePath = path.join(this.templatesPath, `${type}.md`);
|
|
81
|
+
const templateExists = await fs.pathExists(templatePath);
|
|
82
|
+
|
|
83
|
+
if (!templateExists) {
|
|
84
|
+
throw new Error(`Template not found for type: ${type}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let template = await fs.readFile(templatePath, 'utf8');
|
|
88
|
+
|
|
89
|
+
// Prepare variables
|
|
90
|
+
const variables = {
|
|
91
|
+
type,
|
|
92
|
+
name: options.name || type,
|
|
93
|
+
created: new Date().toISOString(),
|
|
94
|
+
...options
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Replace template variables
|
|
98
|
+
let content = template;
|
|
99
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
100
|
+
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
101
|
+
content = content.replace(regex, String(value));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Ensure context directory exists
|
|
105
|
+
await fs.ensureDir(this.contextPath);
|
|
106
|
+
|
|
107
|
+
// Determine output path
|
|
108
|
+
const contextFile = options.name
|
|
109
|
+
? `${options.name.toLowerCase().replace(/\s+/g, '-')}.md`
|
|
110
|
+
: `${type}.md`;
|
|
111
|
+
const contextFilePath = path.join(this.contextPath, contextFile);
|
|
112
|
+
|
|
113
|
+
// Write context file
|
|
114
|
+
await fs.writeFile(contextFilePath, content);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
path: contextFilePath,
|
|
118
|
+
content,
|
|
119
|
+
created: variables.created,
|
|
120
|
+
type
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==========================================
|
|
125
|
+
// 2. CONTEXT PRIMING (1 METHOD)
|
|
126
|
+
// ==========================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate comprehensive project snapshot
|
|
130
|
+
*
|
|
131
|
+
* @param {object} options - Prime options
|
|
132
|
+
* @param {boolean} [options.includeGit] - Include git information
|
|
133
|
+
* @param {string} [options.output] - Output file path
|
|
134
|
+
* @returns {Promise<{contexts, summary, timestamp, git?}>}
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* await primeContext({ includeGit: true, output: './snapshot.md' })
|
|
138
|
+
*/
|
|
139
|
+
async primeContext(options = {}) {
|
|
140
|
+
const timestamp = new Date().toISOString();
|
|
141
|
+
const contexts = {
|
|
142
|
+
epics: [],
|
|
143
|
+
issues: [],
|
|
144
|
+
prds: []
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Gather epics
|
|
148
|
+
const epicsExists = await fs.pathExists(this.epicsPath);
|
|
149
|
+
if (epicsExists) {
|
|
150
|
+
const epicFiles = await fs.readdir(this.epicsPath);
|
|
151
|
+
for (const file of epicFiles) {
|
|
152
|
+
if (file.endsWith('.md')) {
|
|
153
|
+
const content = await fs.readFile(
|
|
154
|
+
path.join(this.epicsPath, file),
|
|
155
|
+
'utf8'
|
|
156
|
+
);
|
|
157
|
+
contexts.epics.push({
|
|
158
|
+
file,
|
|
159
|
+
content,
|
|
160
|
+
metadata: this._parseFrontmatter(content)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Gather issues
|
|
167
|
+
const issuesExists = await fs.pathExists(this.issuesPath);
|
|
168
|
+
if (issuesExists) {
|
|
169
|
+
const issueFiles = await fs.readdir(this.issuesPath);
|
|
170
|
+
for (const file of issueFiles) {
|
|
171
|
+
if (/^\d+\.md$/.test(file)) {
|
|
172
|
+
const content = await fs.readFile(
|
|
173
|
+
path.join(this.issuesPath, file),
|
|
174
|
+
'utf8'
|
|
175
|
+
);
|
|
176
|
+
contexts.issues.push({
|
|
177
|
+
file,
|
|
178
|
+
content,
|
|
179
|
+
metadata: this._parseFrontmatter(content)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Generate summary
|
|
186
|
+
let summary = 'Project Snapshot\n';
|
|
187
|
+
summary += `Generated: ${timestamp}\n\n`;
|
|
188
|
+
summary += `Epics: ${contexts.epics.length}\n`;
|
|
189
|
+
summary += `Issues: ${contexts.issues.length}\n`;
|
|
190
|
+
|
|
191
|
+
if (contexts.epics.length === 0 && contexts.issues.length === 0) {
|
|
192
|
+
summary += '\nProject appears to be empty or newly initialized.';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Include git info if requested
|
|
196
|
+
let git;
|
|
197
|
+
if (options.includeGit) {
|
|
198
|
+
git = await this._getGitInfo();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Write to output if specified
|
|
202
|
+
if (options.output) {
|
|
203
|
+
const outputContent = this._generateSnapshotContent(contexts, summary, git);
|
|
204
|
+
await fs.writeFile(options.output, outputContent);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
contexts,
|
|
209
|
+
summary,
|
|
210
|
+
timestamp,
|
|
211
|
+
...(git && { git })
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ==========================================
|
|
216
|
+
// 3. CONTEXT UPDATES (1 METHOD)
|
|
217
|
+
// ==========================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update existing context
|
|
221
|
+
*
|
|
222
|
+
* @param {string} type - Context type to update
|
|
223
|
+
* @param {object} updates - Update data
|
|
224
|
+
* @param {string} [updates.mode] - Update mode: 'append' or 'replace'
|
|
225
|
+
* @param {string} updates.content - New content
|
|
226
|
+
* @returns {Promise<{updated, changes, timestamp}>}
|
|
227
|
+
* @throws {Error} If context not found
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* await updateContext('project-brief', {
|
|
231
|
+
* mode: 'append',
|
|
232
|
+
* content: '## New Section'
|
|
233
|
+
* })
|
|
234
|
+
*/
|
|
235
|
+
async updateContext(type, updates = {}) {
|
|
236
|
+
const contextFile = `${type}.md`;
|
|
237
|
+
const contextPath = path.join(this.contextPath, contextFile);
|
|
238
|
+
|
|
239
|
+
// Check if context exists
|
|
240
|
+
const exists = await fs.pathExists(contextPath);
|
|
241
|
+
if (!exists) {
|
|
242
|
+
throw new Error(`Context not found: ${type}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const mode = updates.mode || 'append';
|
|
246
|
+
let newContent;
|
|
247
|
+
|
|
248
|
+
if (mode === 'replace') {
|
|
249
|
+
newContent = updates.content;
|
|
250
|
+
} else {
|
|
251
|
+
// Append mode
|
|
252
|
+
const existingContent = await fs.readFile(contextPath, 'utf8');
|
|
253
|
+
newContent = existingContent + updates.content;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Write updated content
|
|
257
|
+
await fs.writeFile(contextPath, newContent);
|
|
258
|
+
|
|
259
|
+
const timestamp = new Date().toISOString();
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
updated: true,
|
|
263
|
+
changes: {
|
|
264
|
+
mode,
|
|
265
|
+
timestamp
|
|
266
|
+
},
|
|
267
|
+
timestamp
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ==========================================
|
|
272
|
+
// 4. CONTEXT READING (2 METHODS)
|
|
273
|
+
// ==========================================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Read context file
|
|
277
|
+
*
|
|
278
|
+
* @param {string} type - Context type
|
|
279
|
+
* @returns {Promise<{type, content, metadata, updated}>}
|
|
280
|
+
* @throws {Error} If context not found
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* const context = await getContext('project-brief')
|
|
284
|
+
*/
|
|
285
|
+
async getContext(type) {
|
|
286
|
+
const contextFile = `${type}.md`;
|
|
287
|
+
const contextPath = path.join(this.contextPath, contextFile);
|
|
288
|
+
|
|
289
|
+
// Check if context exists
|
|
290
|
+
const exists = await fs.pathExists(contextPath);
|
|
291
|
+
if (!exists) {
|
|
292
|
+
throw new Error(`Context not found: ${type}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const content = await fs.readFile(contextPath, 'utf8');
|
|
296
|
+
const metadata = this._parseFrontmatter(content);
|
|
297
|
+
const stats = await fs.stat(contextPath);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
type,
|
|
301
|
+
content,
|
|
302
|
+
metadata,
|
|
303
|
+
updated: stats.mtime.toISOString()
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* List all contexts
|
|
309
|
+
*
|
|
310
|
+
* @returns {Promise<{contexts, byType}>}
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* const { contexts, byType } = await listContexts()
|
|
314
|
+
*/
|
|
315
|
+
async listContexts() {
|
|
316
|
+
const exists = await fs.pathExists(this.contextPath);
|
|
317
|
+
if (!exists) {
|
|
318
|
+
return {
|
|
319
|
+
contexts: [],
|
|
320
|
+
byType: {}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const files = await fs.readdir(this.contextPath);
|
|
325
|
+
const contexts = [];
|
|
326
|
+
const byType = {};
|
|
327
|
+
|
|
328
|
+
for (const file of files) {
|
|
329
|
+
if (file.endsWith('.md')) {
|
|
330
|
+
const filePath = path.join(this.contextPath, file);
|
|
331
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
332
|
+
const metadata = this._parseFrontmatter(content);
|
|
333
|
+
const stats = await fs.stat(filePath);
|
|
334
|
+
|
|
335
|
+
const contextType = metadata.type || 'unknown';
|
|
336
|
+
|
|
337
|
+
const contextInfo = {
|
|
338
|
+
file,
|
|
339
|
+
type: contextType,
|
|
340
|
+
metadata,
|
|
341
|
+
updated: stats.mtime.toISOString(),
|
|
342
|
+
size: stats.size
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
contexts.push(contextInfo);
|
|
346
|
+
|
|
347
|
+
if (!byType[contextType]) {
|
|
348
|
+
byType[contextType] = [];
|
|
349
|
+
}
|
|
350
|
+
byType[contextType].push(contextInfo);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
contexts,
|
|
356
|
+
byType
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ==========================================
|
|
361
|
+
// 5. CONTEXT VALIDATION (1 METHOD)
|
|
362
|
+
// ==========================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Validate context structure
|
|
366
|
+
*
|
|
367
|
+
* @param {string} type - Context type
|
|
368
|
+
* @param {string} content - Context content
|
|
369
|
+
* @returns {Promise<{valid, errors}>}
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* const { valid, errors } = await validateContext('project-brief', content)
|
|
373
|
+
*/
|
|
374
|
+
async validateContext(type, content) {
|
|
375
|
+
const errors = [];
|
|
376
|
+
|
|
377
|
+
// Check for frontmatter
|
|
378
|
+
if (!content.startsWith('---')) {
|
|
379
|
+
errors.push('Missing frontmatter');
|
|
380
|
+
return { valid: false, errors };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Parse frontmatter
|
|
384
|
+
const metadata = this._parseFrontmatter(content);
|
|
385
|
+
|
|
386
|
+
// Validate required fields based on type
|
|
387
|
+
const requiredFields = {
|
|
388
|
+
'project-brief': ['type', 'name', 'created'],
|
|
389
|
+
'progress': ['type', 'name'],
|
|
390
|
+
'tech-context': ['type', 'name'],
|
|
391
|
+
'default': ['type']
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const required = requiredFields[type] || requiredFields.default;
|
|
395
|
+
|
|
396
|
+
for (const field of required) {
|
|
397
|
+
if (!metadata[field]) {
|
|
398
|
+
errors.push(`Missing required field: ${field}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
valid: errors.length === 0,
|
|
404
|
+
errors
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ==========================================
|
|
409
|
+
// 6. CONTEXT OPERATIONS (2 METHODS)
|
|
410
|
+
// ==========================================
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Merge multiple contexts
|
|
414
|
+
*
|
|
415
|
+
* @param {Array} contexts - Array of context objects
|
|
416
|
+
* @returns {Promise<{merged, sources}>}
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* const result = await mergeContexts([context1, context2])
|
|
420
|
+
*/
|
|
421
|
+
async mergeContexts(contexts) {
|
|
422
|
+
const sources = contexts.map(c => c.type);
|
|
423
|
+
let merged = '';
|
|
424
|
+
const seenSections = new Set();
|
|
425
|
+
|
|
426
|
+
for (const context of contexts) {
|
|
427
|
+
const lines = context.content.split('\n');
|
|
428
|
+
|
|
429
|
+
for (const line of lines) {
|
|
430
|
+
// Track sections to avoid duplicates
|
|
431
|
+
if (line.startsWith('##')) {
|
|
432
|
+
const sectionKey = line.trim();
|
|
433
|
+
if (seenSections.has(sectionKey)) {
|
|
434
|
+
continue; // Skip duplicate section
|
|
435
|
+
}
|
|
436
|
+
seenSections.add(sectionKey);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
merged += line + '\n';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
merged += '\n---\n\n';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
merged: merged.trim(),
|
|
447
|
+
sources
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Analyze context usage
|
|
453
|
+
*
|
|
454
|
+
* @returns {Promise<{stats, recommendations}>}
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* const { stats, recommendations } = await analyzeContextUsage()
|
|
458
|
+
*/
|
|
459
|
+
async analyzeContextUsage() {
|
|
460
|
+
const { contexts } = await this.listContexts();
|
|
461
|
+
|
|
462
|
+
let totalSize = 0;
|
|
463
|
+
const recommendations = [];
|
|
464
|
+
|
|
465
|
+
for (const context of contexts) {
|
|
466
|
+
totalSize += context.size;
|
|
467
|
+
|
|
468
|
+
// Check for old contexts (> 90 days)
|
|
469
|
+
const updated = new Date(context.updated);
|
|
470
|
+
const daysSinceUpdate = Math.floor(
|
|
471
|
+
(Date.now() - updated.getTime()) / (1000 * 60 * 60 * 24)
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (daysSinceUpdate > 90) {
|
|
475
|
+
recommendations.push(
|
|
476
|
+
`Context "${context.file}" is ${daysSinceUpdate} days old - consider archiving or updating`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check for large contexts (> 50KB)
|
|
481
|
+
if (context.size > 50000) {
|
|
482
|
+
recommendations.push(
|
|
483
|
+
`Context "${context.file}" is ${Math.round(context.size / 1024)}KB - consider splitting into smaller contexts`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const stats = {
|
|
489
|
+
totalContexts: contexts.length,
|
|
490
|
+
totalSize,
|
|
491
|
+
averageSize: contexts.length > 0 ? Math.round(totalSize / contexts.length) : 0
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
stats,
|
|
496
|
+
recommendations
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ==========================================
|
|
501
|
+
// PRIVATE HELPER METHODS
|
|
502
|
+
// ==========================================
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Parse YAML frontmatter from content
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
_parseFrontmatter(content) {
|
|
509
|
+
if (!content || !content.startsWith('---')) {
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
514
|
+
if (!match) {
|
|
515
|
+
return {};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const metadata = {};
|
|
519
|
+
const lines = match[1].split('\n');
|
|
520
|
+
|
|
521
|
+
for (const line of lines) {
|
|
522
|
+
const colonIndex = line.indexOf(':');
|
|
523
|
+
if (colonIndex === -1) continue;
|
|
524
|
+
|
|
525
|
+
const key = line.substring(0, colonIndex).trim();
|
|
526
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
527
|
+
|
|
528
|
+
if (key) {
|
|
529
|
+
metadata[key] = value;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return metadata;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get git information
|
|
538
|
+
* @private
|
|
539
|
+
*/
|
|
540
|
+
async _getGitInfo() {
|
|
541
|
+
const { execSync } = require('child_process');
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
545
|
+
const commit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
|
546
|
+
const status = execSync('git status --short', { encoding: 'utf8' }).trim();
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
branch,
|
|
550
|
+
commit,
|
|
551
|
+
status: status || 'clean'
|
|
552
|
+
};
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return {
|
|
555
|
+
error: 'Not a git repository or git not available'
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Generate snapshot content for output
|
|
562
|
+
* @private
|
|
563
|
+
*/
|
|
564
|
+
_generateSnapshotContent(contexts, summary, git) {
|
|
565
|
+
let content = `# Project Snapshot\n\n`;
|
|
566
|
+
content += `${summary}\n\n`;
|
|
567
|
+
|
|
568
|
+
if (git) {
|
|
569
|
+
content += `## Git Information\n`;
|
|
570
|
+
content += `- Branch: ${git.branch}\n`;
|
|
571
|
+
content += `- Commit: ${git.commit}\n`;
|
|
572
|
+
content += `- Status: ${git.status}\n\n`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (contexts.epics.length > 0) {
|
|
576
|
+
content += `## Epics (${contexts.epics.length})\n\n`;
|
|
577
|
+
for (const epic of contexts.epics) {
|
|
578
|
+
content += `### ${epic.file}\n`;
|
|
579
|
+
content += `${epic.content}\n\n---\n\n`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (contexts.issues.length > 0) {
|
|
584
|
+
content += `## Issues (${contexts.issues.length})\n\n`;
|
|
585
|
+
for (const issue of contexts.issues) {
|
|
586
|
+
content += `### ${issue.file}\n`;
|
|
587
|
+
content += `${issue.content}\n\n---\n\n`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return content;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
module.exports = ContextService;
|