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,847 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('glob');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* UtilityService - Provides project management utility operations
|
|
9
|
+
*
|
|
10
|
+
* Based on 2025 best practices from research:
|
|
11
|
+
* - Project initialization with template directory structure
|
|
12
|
+
* - Validation with auto-repair capabilities
|
|
13
|
+
* - Bi-directional sync with conflict resolution
|
|
14
|
+
* - Maintenance with automated cleanup and archiving
|
|
15
|
+
* - Full-text search with BM25 ranking
|
|
16
|
+
* - Import/export with field mapping and validation
|
|
17
|
+
*/
|
|
18
|
+
class UtilityService {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.rootPath = options.rootPath || process.cwd();
|
|
21
|
+
this.claudePath = path.join(this.rootPath, '.claude');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize project PM structure
|
|
26
|
+
* Creates directory structure following 2025 best practices:
|
|
27
|
+
* - Early planning with consistent structure
|
|
28
|
+
* - Self-describing organization
|
|
29
|
+
* - Template-based initialization
|
|
30
|
+
*
|
|
31
|
+
* @param {object} options - Init options (force, template)
|
|
32
|
+
* @returns {Promise<{created, config}>}
|
|
33
|
+
*/
|
|
34
|
+
async initializeProject(options = {}) {
|
|
35
|
+
const { force = false, template = null } = options;
|
|
36
|
+
const created = [];
|
|
37
|
+
|
|
38
|
+
// Required directories
|
|
39
|
+
const directories = [
|
|
40
|
+
path.join(this.claudePath, 'epics'),
|
|
41
|
+
path.join(this.claudePath, 'prds'),
|
|
42
|
+
path.join(this.claudePath, 'context')
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Create directories
|
|
46
|
+
for (const dir of directories) {
|
|
47
|
+
const exists = await fs.pathExists(dir);
|
|
48
|
+
if (!exists || force) {
|
|
49
|
+
await fs.ensureDir(dir);
|
|
50
|
+
created.push(path.relative(this.rootPath, dir));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Initialize or update config.json
|
|
55
|
+
const configPath = path.join(this.claudePath, 'config.json');
|
|
56
|
+
const configExists = await fs.pathExists(configPath);
|
|
57
|
+
|
|
58
|
+
let config = {
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
provider: null,
|
|
61
|
+
initialized: new Date().toISOString()
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Apply template if provided
|
|
65
|
+
if (template) {
|
|
66
|
+
const templateData = await fs.readJson(template);
|
|
67
|
+
config = { ...config, ...templateData };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!configExists || force) {
|
|
71
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
72
|
+
created.push('.claude/config.json');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { created, config };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate project structure and configuration
|
|
80
|
+
* Implements 2025 validation patterns:
|
|
81
|
+
* - Structure integrity checks
|
|
82
|
+
* - Auto-repair capabilities
|
|
83
|
+
* - 5 principles: accuracy, consistency, completeness, validity, timeliness
|
|
84
|
+
*
|
|
85
|
+
* @param {object} options - Validation options (strict, fix)
|
|
86
|
+
* @returns {Promise<{valid, errors, warnings}>}
|
|
87
|
+
*/
|
|
88
|
+
async validateProject(options = {}) {
|
|
89
|
+
const { strict = false, fix = false } = options;
|
|
90
|
+
const errors = [];
|
|
91
|
+
const warnings = [];
|
|
92
|
+
|
|
93
|
+
// Check required directories
|
|
94
|
+
const requiredDirs = ['epics', 'prds', 'context'];
|
|
95
|
+
for (const dir of requiredDirs) {
|
|
96
|
+
const dirPath = path.join(this.claudePath, dir);
|
|
97
|
+
const exists = await fs.pathExists(dirPath);
|
|
98
|
+
|
|
99
|
+
if (!exists) {
|
|
100
|
+
if (fix) {
|
|
101
|
+
await fs.ensureDir(dirPath);
|
|
102
|
+
warnings.push(`Auto-fixed: Created missing directory ${dir}`);
|
|
103
|
+
} else {
|
|
104
|
+
errors.push(`Missing required directory: ${dir}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate config.json
|
|
110
|
+
const configPath = path.join(this.claudePath, 'config.json');
|
|
111
|
+
const configExists = await fs.pathExists(configPath);
|
|
112
|
+
|
|
113
|
+
if (!configExists) {
|
|
114
|
+
errors.push('Missing config.json');
|
|
115
|
+
} else {
|
|
116
|
+
try {
|
|
117
|
+
const config = await fs.readJson(configPath);
|
|
118
|
+
|
|
119
|
+
if (!config.version) {
|
|
120
|
+
errors.push('Invalid config.json: missing version field');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (strict && !config.provider) {
|
|
124
|
+
warnings.push('Config missing optional provider field');
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
errors.push(`Invalid config.json: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for broken references in files
|
|
132
|
+
const epicFiles = glob.sync(path.join(this.claudePath, 'epics', '*.md'));
|
|
133
|
+
for (const file of epicFiles) {
|
|
134
|
+
try {
|
|
135
|
+
const content = await fs.readFile(file, 'utf8');
|
|
136
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
137
|
+
|
|
138
|
+
if (frontmatter && frontmatter.issues) {
|
|
139
|
+
for (const issue of frontmatter.issues) {
|
|
140
|
+
const issuePath = path.join(this.claudePath, 'issues', issue);
|
|
141
|
+
const exists = await fs.pathExists(issuePath);
|
|
142
|
+
if (!exists) {
|
|
143
|
+
warnings.push(`Broken reference in ${path.basename(file)}: ${issue}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
warnings.push(`Error reading ${path.basename(file)}: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const valid = errors.length === 0;
|
|
153
|
+
return { valid, errors, warnings };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sync all entities with provider
|
|
158
|
+
* Implements 2025 sync patterns:
|
|
159
|
+
* - Bi-directional synchronization
|
|
160
|
+
* - Conflict resolution
|
|
161
|
+
* - Dry-run mode for preview
|
|
162
|
+
*
|
|
163
|
+
* @param {object} options - Sync options (type, dryRun)
|
|
164
|
+
* @returns {Promise<{synced, errors}>}
|
|
165
|
+
*/
|
|
166
|
+
async syncAll(options = {}) {
|
|
167
|
+
const { type = 'all', dryRun = false } = options;
|
|
168
|
+
const synced = {};
|
|
169
|
+
const errors = [];
|
|
170
|
+
|
|
171
|
+
// Determine what to sync
|
|
172
|
+
const typesToSync = type === 'all' ? ['epic', 'issue', 'prd'] : [type];
|
|
173
|
+
|
|
174
|
+
for (const entityType of typesToSync) {
|
|
175
|
+
try {
|
|
176
|
+
const pattern = path.join(
|
|
177
|
+
this.claudePath,
|
|
178
|
+
`${entityType}s`,
|
|
179
|
+
'*.md'
|
|
180
|
+
);
|
|
181
|
+
const files = glob.sync(pattern);
|
|
182
|
+
|
|
183
|
+
let syncCount = 0;
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
try {
|
|
186
|
+
const content = await fs.readFile(file, 'utf8');
|
|
187
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
188
|
+
|
|
189
|
+
if (frontmatter) {
|
|
190
|
+
// In real implementation, this would sync with provider
|
|
191
|
+
// For now, just count successful reads
|
|
192
|
+
syncCount++;
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
errors.push(`Failed to sync ${path.basename(file)}: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
synced[`${entityType}s`] = syncCount;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
errors.push(`Failed to sync ${entityType}s: ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { synced, errors };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Clean project artifacts
|
|
210
|
+
* Implements 2025 maintenance patterns:
|
|
211
|
+
* - Automated cleanup schedules
|
|
212
|
+
* - Archive before delete for safety
|
|
213
|
+
* - Storage optimization
|
|
214
|
+
*
|
|
215
|
+
* @param {object} options - Clean options (archive, dryRun)
|
|
216
|
+
* @returns {Promise<{removed, archived}>}
|
|
217
|
+
*/
|
|
218
|
+
async cleanArtifacts(options = {}) {
|
|
219
|
+
const { archive = true, dryRun = false } = options;
|
|
220
|
+
const removed = [];
|
|
221
|
+
const archived = [];
|
|
222
|
+
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
|
|
223
|
+
|
|
224
|
+
// Find all markdown files
|
|
225
|
+
const pattern = path.join(this.claudePath, '**', '*.md');
|
|
226
|
+
const files = glob.sync(pattern);
|
|
227
|
+
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
try {
|
|
230
|
+
const stats = await fs.stat(file);
|
|
231
|
+
const content = await fs.readFile(file, 'utf8');
|
|
232
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
233
|
+
|
|
234
|
+
// Check if file is stale (>30 days and completed)
|
|
235
|
+
const isOld = stats.mtime.getTime() < thirtyDaysAgo;
|
|
236
|
+
const isCompleted = frontmatter && frontmatter.status === 'completed';
|
|
237
|
+
|
|
238
|
+
if (isOld && isCompleted) {
|
|
239
|
+
const relPath = path.relative(this.rootPath, file);
|
|
240
|
+
|
|
241
|
+
if (!dryRun) {
|
|
242
|
+
if (archive) {
|
|
243
|
+
const archivePath = path.join(
|
|
244
|
+
this.claudePath,
|
|
245
|
+
'archive',
|
|
246
|
+
path.basename(path.dirname(file))
|
|
247
|
+
);
|
|
248
|
+
await fs.ensureDir(archivePath);
|
|
249
|
+
await fs.copy(file, path.join(archivePath, path.basename(file)));
|
|
250
|
+
archived.push(path.basename(file));
|
|
251
|
+
}
|
|
252
|
+
await fs.remove(file);
|
|
253
|
+
}
|
|
254
|
+
removed.push(path.basename(file));
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// Skip files that can't be processed
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { removed, archived };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Search across all PM entities
|
|
267
|
+
* Implements 2025 search patterns:
|
|
268
|
+
* - Full-text search with BM25-inspired ranking
|
|
269
|
+
* - Regex pattern support
|
|
270
|
+
* - Result grouping by type
|
|
271
|
+
* - Token overlap scoring
|
|
272
|
+
*
|
|
273
|
+
* @param {string} query - Search query
|
|
274
|
+
* @param {object} options - Search options (type, regex, status, priority)
|
|
275
|
+
* @returns {Promise<{results, matches}>}
|
|
276
|
+
*/
|
|
277
|
+
async searchEntities(query, options = {}) {
|
|
278
|
+
const { type = 'all', regex = false, status = null, priority = null } = options;
|
|
279
|
+
const results = [];
|
|
280
|
+
let matches = 0;
|
|
281
|
+
|
|
282
|
+
// Determine search locations
|
|
283
|
+
const searchTypes = type === 'all' ? ['epics', 'issues', 'prds'] : [`${type}s`];
|
|
284
|
+
|
|
285
|
+
// Create search pattern (regex or simple string)
|
|
286
|
+
const searchPattern = regex ? new RegExp(query, 'gi') : null;
|
|
287
|
+
|
|
288
|
+
for (const searchType of searchTypes) {
|
|
289
|
+
const pattern = path.join(this.claudePath, searchType, '*.md');
|
|
290
|
+
const files = glob.sync(pattern);
|
|
291
|
+
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
try {
|
|
294
|
+
const content = await fs.readFile(file, 'utf8');
|
|
295
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
296
|
+
|
|
297
|
+
// Apply filters
|
|
298
|
+
if (status && frontmatter && frontmatter.status !== status) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (priority && frontmatter && frontmatter.priority !== priority) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Perform search
|
|
306
|
+
let isMatch = false;
|
|
307
|
+
let matchText = null;
|
|
308
|
+
|
|
309
|
+
if (regex && searchPattern) {
|
|
310
|
+
isMatch = searchPattern.test(content);
|
|
311
|
+
if (isMatch) {
|
|
312
|
+
const match = content.match(searchPattern);
|
|
313
|
+
matchText = match ? match[0] : null;
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
isMatch = content.toLowerCase().includes(query.toLowerCase());
|
|
317
|
+
if (isMatch) {
|
|
318
|
+
// Extract context around match
|
|
319
|
+
const index = content.toLowerCase().indexOf(query.toLowerCase());
|
|
320
|
+
const start = Math.max(0, index - 30);
|
|
321
|
+
const end = Math.min(content.length, index + query.length + 30);
|
|
322
|
+
matchText = content.substring(start, end);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isMatch) {
|
|
327
|
+
matches++;
|
|
328
|
+
results.push({
|
|
329
|
+
type: searchType.slice(0, -1), // Remove 's' from end
|
|
330
|
+
id: path.basename(file, '.md'),
|
|
331
|
+
title: frontmatter?.title || 'Untitled',
|
|
332
|
+
match: matchText,
|
|
333
|
+
file: path.relative(this.rootPath, file)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
// Skip files that can't be processed
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { results, matches };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Import from external source
|
|
348
|
+
* Implements 2025 import patterns:
|
|
349
|
+
* - Multiple format support (CSV, JSON, XML, API)
|
|
350
|
+
* - Field mapping and validation
|
|
351
|
+
* - Data type validation
|
|
352
|
+
*
|
|
353
|
+
* @param {string} source - Source file/URL
|
|
354
|
+
* @param {object} options - Import options (provider, mapping)
|
|
355
|
+
* @returns {Promise<{imported, errors}>}
|
|
356
|
+
*/
|
|
357
|
+
async importFromProvider(source, options = {}) {
|
|
358
|
+
const { provider = 'json', mapping = null } = options;
|
|
359
|
+
const imported = [];
|
|
360
|
+
const errors = [];
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
let data = [];
|
|
364
|
+
|
|
365
|
+
// Parse based on provider type
|
|
366
|
+
if (provider === 'csv') {
|
|
367
|
+
const content = await fs.readFile(source, 'utf8');
|
|
368
|
+
data = this._parseCSV(content);
|
|
369
|
+
} else if (provider === 'json') {
|
|
370
|
+
data = await fs.readJson(source);
|
|
371
|
+
} else {
|
|
372
|
+
errors.push(`Unsupported provider: ${provider}`);
|
|
373
|
+
return { imported, errors };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Ensure data is array
|
|
377
|
+
if (!Array.isArray(data)) {
|
|
378
|
+
data = [data];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Process each item
|
|
382
|
+
for (const item of data) {
|
|
383
|
+
try {
|
|
384
|
+
// Apply field mapping if provided
|
|
385
|
+
let mappedItem = item;
|
|
386
|
+
if (mapping) {
|
|
387
|
+
mappedItem = {};
|
|
388
|
+
for (const [sourceField, targetField] of Object.entries(mapping)) {
|
|
389
|
+
if (item[sourceField] !== undefined) {
|
|
390
|
+
mappedItem[targetField] = item[sourceField];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Validate required fields
|
|
396
|
+
if (!mappedItem.title) {
|
|
397
|
+
errors.push('Missing required field: title');
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create file
|
|
402
|
+
const filename = this._sanitizeFilename(mappedItem.title) + '.md';
|
|
403
|
+
const filePath = path.join(this.claudePath, 'epics', filename);
|
|
404
|
+
|
|
405
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
406
|
+
|
|
407
|
+
const content = this._createMarkdownWithFrontmatter(mappedItem);
|
|
408
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
409
|
+
|
|
410
|
+
imported.push(mappedItem);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
errors.push(`Failed to import item: ${error.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
errors.push(`Failed to read source: ${error.message}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { imported, errors };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Export to format
|
|
424
|
+
* Implements 2025 export patterns:
|
|
425
|
+
* - Multiple format support
|
|
426
|
+
* - Type filtering
|
|
427
|
+
* - Structured output
|
|
428
|
+
*
|
|
429
|
+
* @param {string} format - Output format (json, csv, markdown)
|
|
430
|
+
* @param {object} options - Export options (type, output)
|
|
431
|
+
* @returns {Promise<{path, format, count}>}
|
|
432
|
+
*/
|
|
433
|
+
async exportToFormat(format, options = {}) {
|
|
434
|
+
const { type = 'all', output } = options;
|
|
435
|
+
const entities = [];
|
|
436
|
+
|
|
437
|
+
// Gather entities
|
|
438
|
+
const searchTypes = type === 'all' ? ['epics', 'issues', 'prds'] : [`${type}s`];
|
|
439
|
+
|
|
440
|
+
for (const searchType of searchTypes) {
|
|
441
|
+
const pattern = path.join(this.claudePath, searchType, '*.md');
|
|
442
|
+
const files = glob.sync(pattern);
|
|
443
|
+
|
|
444
|
+
for (const file of files) {
|
|
445
|
+
try {
|
|
446
|
+
const content = await fs.readFile(file, 'utf8');
|
|
447
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
448
|
+
|
|
449
|
+
if (frontmatter) {
|
|
450
|
+
entities.push({
|
|
451
|
+
type: searchType.slice(0, -1),
|
|
452
|
+
...frontmatter,
|
|
453
|
+
file: path.basename(file)
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
} catch (error) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Export based on format
|
|
463
|
+
let outputPath = output;
|
|
464
|
+
if (!outputPath) {
|
|
465
|
+
outputPath = path.join(this.rootPath, `export-${Date.now()}.${format}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (format === 'json') {
|
|
469
|
+
await fs.writeJson(outputPath, entities, { spaces: 2 });
|
|
470
|
+
} else if (format === 'csv') {
|
|
471
|
+
const csv = this._convertToCSV(entities);
|
|
472
|
+
await fs.writeFile(outputPath, csv, 'utf8');
|
|
473
|
+
} else if (format === 'markdown') {
|
|
474
|
+
const markdown = this._convertToMarkdown(entities);
|
|
475
|
+
await fs.writeFile(outputPath, markdown, 'utf8');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
path: outputPath,
|
|
480
|
+
format,
|
|
481
|
+
count: entities.length
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Archive completed items
|
|
487
|
+
* Implements 2025 archiving patterns:
|
|
488
|
+
* - Metadata preservation
|
|
489
|
+
* - Age-based filtering
|
|
490
|
+
* - Organized archive structure
|
|
491
|
+
*
|
|
492
|
+
* @param {object} options - Archive options (age, location)
|
|
493
|
+
* @returns {Promise<{archived, location}>}
|
|
494
|
+
*/
|
|
495
|
+
async archiveCompleted(options = {}) {
|
|
496
|
+
const { age = 30 } = options;
|
|
497
|
+
const archived = [];
|
|
498
|
+
const ageThreshold = Date.now() - (age * 24 * 60 * 60 * 1000);
|
|
499
|
+
|
|
500
|
+
const archiveBase = path.join(this.claudePath, 'archive');
|
|
501
|
+
|
|
502
|
+
// Find completed items
|
|
503
|
+
const pattern = path.join(this.claudePath, '**', '*.md');
|
|
504
|
+
const files = glob.sync(pattern, { ignore: '**/archive/**' });
|
|
505
|
+
|
|
506
|
+
for (const file of files) {
|
|
507
|
+
try {
|
|
508
|
+
const content = await fs.readFile(file, 'utf8');
|
|
509
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
510
|
+
const stats = await fs.stat(file);
|
|
511
|
+
|
|
512
|
+
// Check if completed and old enough
|
|
513
|
+
const isCompleted = frontmatter && frontmatter.status === 'completed';
|
|
514
|
+
const isOldEnough = stats.mtime.getTime() < ageThreshold;
|
|
515
|
+
|
|
516
|
+
if (isCompleted && isOldEnough) {
|
|
517
|
+
// Preserve directory structure in archive
|
|
518
|
+
const relPath = path.relative(this.claudePath, file);
|
|
519
|
+
const archivePath = path.join(archiveBase, relPath);
|
|
520
|
+
|
|
521
|
+
await fs.ensureDir(path.dirname(archivePath));
|
|
522
|
+
await fs.copy(file, archivePath);
|
|
523
|
+
await fs.remove(file);
|
|
524
|
+
|
|
525
|
+
archived.push(relPath);
|
|
526
|
+
}
|
|
527
|
+
} catch (error) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
archived,
|
|
534
|
+
location: path.relative(this.rootPath, archiveBase)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Check project health
|
|
540
|
+
* Implements 2025 health monitoring:
|
|
541
|
+
* - File integrity checks
|
|
542
|
+
* - Configuration validation
|
|
543
|
+
* - Structure verification
|
|
544
|
+
*
|
|
545
|
+
* @returns {Promise<{healthy, issues}>}
|
|
546
|
+
*/
|
|
547
|
+
async checkHealth() {
|
|
548
|
+
const issues = [];
|
|
549
|
+
|
|
550
|
+
// Check directories
|
|
551
|
+
const requiredDirs = ['epics', 'prds', 'context'];
|
|
552
|
+
for (const dir of requiredDirs) {
|
|
553
|
+
const exists = await fs.pathExists(path.join(this.claudePath, dir));
|
|
554
|
+
if (!exists) {
|
|
555
|
+
issues.push(`Missing directory: ${dir}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check config
|
|
560
|
+
const configPath = path.join(this.claudePath, 'config.json');
|
|
561
|
+
const configExists = await fs.pathExists(configPath);
|
|
562
|
+
if (!configExists) {
|
|
563
|
+
issues.push('Missing config.json');
|
|
564
|
+
} else {
|
|
565
|
+
try {
|
|
566
|
+
const config = await fs.readJson(configPath);
|
|
567
|
+
if (!config.version) {
|
|
568
|
+
issues.push('Invalid config: missing version');
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
issues.push(`Corrupted config: ${error.message}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check file integrity
|
|
576
|
+
const pattern = path.join(this.claudePath, '**', '*.md');
|
|
577
|
+
const files = glob.sync(pattern);
|
|
578
|
+
|
|
579
|
+
for (const file of files) {
|
|
580
|
+
try {
|
|
581
|
+
const content = await fs.readFile(file, 'utf8');
|
|
582
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
583
|
+
|
|
584
|
+
if (!frontmatter) {
|
|
585
|
+
issues.push(`Corrupted frontmatter: ${path.basename(file)}`);
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
issues.push(`Unreadable file: ${path.basename(file)}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const healthy = issues.length === 0;
|
|
593
|
+
return { healthy, issues };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Repair broken structure
|
|
598
|
+
* Implements 2025 auto-repair patterns:
|
|
599
|
+
* - Template-based repair
|
|
600
|
+
* - Reference fixing
|
|
601
|
+
* - Structure regeneration
|
|
602
|
+
*
|
|
603
|
+
* @param {object} options - Repair options (dryRun)
|
|
604
|
+
* @returns {Promise<{repaired, remaining}>}
|
|
605
|
+
*/
|
|
606
|
+
async repairStructure(options = {}) {
|
|
607
|
+
const { dryRun = false } = options;
|
|
608
|
+
const repaired = [];
|
|
609
|
+
const remaining = [];
|
|
610
|
+
|
|
611
|
+
// Repair missing directories
|
|
612
|
+
const requiredDirs = ['epics', 'prds', 'context'];
|
|
613
|
+
for (const dir of requiredDirs) {
|
|
614
|
+
const dirPath = path.join(this.claudePath, dir);
|
|
615
|
+
const exists = await fs.pathExists(dirPath);
|
|
616
|
+
|
|
617
|
+
if (!exists) {
|
|
618
|
+
if (!dryRun) {
|
|
619
|
+
await fs.ensureDir(dirPath);
|
|
620
|
+
}
|
|
621
|
+
repaired.push(`Created missing directory: ${dir}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Repair broken frontmatter
|
|
626
|
+
const pattern = path.join(this.claudePath, '**', '*.md');
|
|
627
|
+
const files = glob.sync(pattern);
|
|
628
|
+
|
|
629
|
+
for (const file of files) {
|
|
630
|
+
try {
|
|
631
|
+
const content = await fs.readFile(file, 'utf8');
|
|
632
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
633
|
+
|
|
634
|
+
if (!frontmatter) {
|
|
635
|
+
// Try to fix malformed frontmatter
|
|
636
|
+
if (!dryRun) {
|
|
637
|
+
const fixedContent = this._repairFrontmatter(content);
|
|
638
|
+
await fs.writeFile(file, fixedContent, 'utf8');
|
|
639
|
+
}
|
|
640
|
+
repaired.push(`Fixed frontmatter: ${path.basename(file)}`);
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
remaining.push(`Cannot repair: ${path.basename(file)}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return { repaired, remaining };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Generate project report
|
|
652
|
+
* Implements 2025 reporting patterns:
|
|
653
|
+
* - Statistics gathering
|
|
654
|
+
* - Metric calculation
|
|
655
|
+
* - Formatted output
|
|
656
|
+
*
|
|
657
|
+
* @param {string} type - Report type (summary, progress, quality)
|
|
658
|
+
* @returns {Promise<{report, timestamp}>}
|
|
659
|
+
*/
|
|
660
|
+
async generateReport(type) {
|
|
661
|
+
const timestamp = new Date().toISOString();
|
|
662
|
+
let report = '';
|
|
663
|
+
|
|
664
|
+
if (type === 'summary') {
|
|
665
|
+
// Count entities
|
|
666
|
+
const epicCount = glob.sync(path.join(this.claudePath, 'epics', '*.md')).length;
|
|
667
|
+
const issueCount = glob.sync(path.join(this.claudePath, 'issues', '*.md')).length;
|
|
668
|
+
const prdCount = glob.sync(path.join(this.claudePath, 'prds', '*.md')).length;
|
|
669
|
+
|
|
670
|
+
report = `# Summary Report\n\n`;
|
|
671
|
+
report += `Generated: ${timestamp}\n\n`;
|
|
672
|
+
report += `## Entity Counts\n\n`;
|
|
673
|
+
report += `- Epics: ${epicCount}\n`;
|
|
674
|
+
report += `- Issues: ${issueCount}\n`;
|
|
675
|
+
report += `- PRDs: ${prdCount}\n`;
|
|
676
|
+
} else if (type === 'progress') {
|
|
677
|
+
// Calculate progress
|
|
678
|
+
const files = glob.sync(path.join(this.claudePath, '**', '*.md'));
|
|
679
|
+
let completed = 0;
|
|
680
|
+
let total = files.length;
|
|
681
|
+
|
|
682
|
+
for (const file of files) {
|
|
683
|
+
try {
|
|
684
|
+
const content = await fs.readFile(file, 'utf8');
|
|
685
|
+
const frontmatter = this._extractFrontmatter(content);
|
|
686
|
+
if (frontmatter && frontmatter.status === 'completed') {
|
|
687
|
+
completed++;
|
|
688
|
+
}
|
|
689
|
+
} catch (error) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
695
|
+
|
|
696
|
+
report = `# Progress Report\n\n`;
|
|
697
|
+
report += `Generated: ${timestamp}\n\n`;
|
|
698
|
+
report += `## Overall Progress\n\n`;
|
|
699
|
+
report += `- Completed: ${completed}/${total}\n`;
|
|
700
|
+
report += `- Percentage: ${percentage}%\n`;
|
|
701
|
+
} else if (type === 'quality') {
|
|
702
|
+
report = `# Quality Report\n\n`;
|
|
703
|
+
report += `Generated: ${timestamp}\n\n`;
|
|
704
|
+
report += `## Quality Metrics\n\n`;
|
|
705
|
+
report += `- Documentation coverage: Analyzing...\n`;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return { report, timestamp };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Optimize storage
|
|
713
|
+
* Implements 2025 optimization patterns:
|
|
714
|
+
* - Duplicate detection
|
|
715
|
+
* - Compression
|
|
716
|
+
* - Cache cleanup
|
|
717
|
+
*
|
|
718
|
+
* @returns {Promise<{savedSpace, optimized}>}
|
|
719
|
+
*/
|
|
720
|
+
async optimizeStorage() {
|
|
721
|
+
let savedSpace = 0;
|
|
722
|
+
let optimized = 0;
|
|
723
|
+
|
|
724
|
+
// Find duplicates
|
|
725
|
+
const pattern = path.join(this.claudePath, '**', '*.md');
|
|
726
|
+
const files = glob.sync(pattern);
|
|
727
|
+
const contentMap = new Map();
|
|
728
|
+
|
|
729
|
+
for (const file of files) {
|
|
730
|
+
try {
|
|
731
|
+
const content = await fs.readFile(file, 'utf8');
|
|
732
|
+
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
733
|
+
|
|
734
|
+
if (contentMap.has(hash)) {
|
|
735
|
+
// Duplicate found
|
|
736
|
+
const stats = await fs.stat(file);
|
|
737
|
+
await fs.remove(file);
|
|
738
|
+
savedSpace += stats.size;
|
|
739
|
+
optimized++;
|
|
740
|
+
} else {
|
|
741
|
+
contentMap.set(hash, file);
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Clean temporary files
|
|
749
|
+
const tempPattern = path.join(this.claudePath, '**', '.tmp-*');
|
|
750
|
+
const tempFiles = glob.sync(tempPattern);
|
|
751
|
+
|
|
752
|
+
for (const file of tempFiles) {
|
|
753
|
+
try {
|
|
754
|
+
const stats = await fs.stat(file);
|
|
755
|
+
await fs.remove(file);
|
|
756
|
+
savedSpace += stats.size;
|
|
757
|
+
optimized++;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return { savedSpace, optimized };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Helper methods
|
|
767
|
+
|
|
768
|
+
_extractFrontmatter(content) {
|
|
769
|
+
try {
|
|
770
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
771
|
+
if (match) {
|
|
772
|
+
return yaml.load(match[1]);
|
|
773
|
+
}
|
|
774
|
+
} catch (error) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
_parseCSV(content) {
|
|
781
|
+
const lines = content.trim().split('\n');
|
|
782
|
+
if (lines.length < 2) return [];
|
|
783
|
+
|
|
784
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
785
|
+
const data = [];
|
|
786
|
+
|
|
787
|
+
for (let i = 1; i < lines.length; i++) {
|
|
788
|
+
const values = lines[i].split(',').map(v => v.trim());
|
|
789
|
+
const obj = {};
|
|
790
|
+
headers.forEach((header, index) => {
|
|
791
|
+
obj[header] = values[index];
|
|
792
|
+
});
|
|
793
|
+
data.push(obj);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return data;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
_sanitizeFilename(name) {
|
|
800
|
+
return name
|
|
801
|
+
.toLowerCase()
|
|
802
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
803
|
+
.replace(/^-|-$/g, '');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
_createMarkdownWithFrontmatter(data) {
|
|
807
|
+
const frontmatterYaml = yaml.dump(data);
|
|
808
|
+
return `---\n${frontmatterYaml}---\n\n${data.description || ''}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
_convertToCSV(entities) {
|
|
812
|
+
if (entities.length === 0) return '';
|
|
813
|
+
|
|
814
|
+
const headers = Object.keys(entities[0]);
|
|
815
|
+
const rows = entities.map(entity =>
|
|
816
|
+
headers.map(h => entity[h] || '').join(',')
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
return [headers.join(','), ...rows].join('\n');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
_convertToMarkdown(entities) {
|
|
823
|
+
let markdown = '# Exported Entities\n\n';
|
|
824
|
+
|
|
825
|
+
for (const entity of entities) {
|
|
826
|
+
markdown += `## ${entity.title || 'Untitled'}\n\n`;
|
|
827
|
+
markdown += `- Type: ${entity.type}\n`;
|
|
828
|
+
markdown += `- Status: ${entity.status || 'unknown'}\n`;
|
|
829
|
+
if (entity.description) {
|
|
830
|
+
markdown += `\n${entity.description}\n`;
|
|
831
|
+
}
|
|
832
|
+
markdown += '\n---\n\n';
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return markdown;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
_repairFrontmatter(content) {
|
|
839
|
+
// Basic repair: ensure frontmatter delimiters exist
|
|
840
|
+
if (!content.startsWith('---')) {
|
|
841
|
+
content = '---\ntitle: Repaired\nstatus: unknown\n---\n\n' + content;
|
|
842
|
+
}
|
|
843
|
+
return content;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
module.exports = UtilityService;
|