devassist-agent 1.0.4 → 1.0.5
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/devassist.js +1 -0
- package/package.json +1 -1
- package/src/cli/commands/report.js +104 -0
- package/src/cli/commands/watch.js +83 -24
package/bin/devassist.js
CHANGED
|
@@ -172,6 +172,7 @@ function printHelp() {
|
|
|
172
172
|
console.log(` ${c.green('watch')} 实时监控文件变更并运行检查`);
|
|
173
173
|
console.log(` ${c.green('diff')} ${c.gray('--staged|--base <分支>')} 仅检查变更文件(CI 模式)`);
|
|
174
174
|
console.log(` ${c.green('report')} ${c.gray('[--open]')} 生成 HTML 检查报告`);
|
|
175
|
+
console.log(` ${c.gray(' --watch-history [n]')} 查看最近 n 条监控复盘报告(默认 20)`);
|
|
175
176
|
console.log(` ${c.green('deploy')} ${c.gray('--check')} 部署前验证(端口 + 路由 + MIME + 清单)`);
|
|
176
177
|
console.log(` ${c.green('debt')} ${c.gray('--scan')} 查看技术债务看板(按优先级排序)`);
|
|
177
178
|
console.log(` ${c.green('convention')} ${c.gray('add|list|remove')} 管理项目约定`);
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* devassist report # Generate report
|
|
13
13
|
* devassist report --ai # Include AI analysis
|
|
14
14
|
* devassist report --open # Open in browser after generating
|
|
15
|
+
* devassist report --watch-history [n] # Show recent watch reports (default 20)
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const fs = require('fs');
|
|
@@ -38,6 +39,11 @@ module.exports = async function report(projectRoot, args, ui) {
|
|
|
38
39
|
const useAI = args.includes('--ai');
|
|
39
40
|
const openBrowser = args.includes('--open');
|
|
40
41
|
|
|
42
|
+
// Watch history mode
|
|
43
|
+
if (args.includes('--watch-history')) {
|
|
44
|
+
return showWatchHistory(projectRoot, args, ui);
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
printHeader('生成报告');
|
|
42
48
|
|
|
43
49
|
// Collect files using shared utility
|
|
@@ -308,3 +314,101 @@ function findingHTML(f) {
|
|
|
308
314
|
function escapeHTML(str) {
|
|
309
315
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
310
316
|
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Show watch history for retrospective.
|
|
320
|
+
* Usage: devassist report --watch-history [n]
|
|
321
|
+
*/
|
|
322
|
+
function showWatchHistory(projectRoot, args, ui) {
|
|
323
|
+
const { printHeader, c } = ui;
|
|
324
|
+
printHeader('监控复盘报告');
|
|
325
|
+
|
|
326
|
+
const historyDir = path.join(projectRoot, '.devassist', 'history', 'watch');
|
|
327
|
+
const logPath = path.join(historyDir, 'watch-log.jsonl');
|
|
328
|
+
|
|
329
|
+
if (!fs.existsSync(logPath)) {
|
|
330
|
+
console.log(` ${c.yellow('暂无监控历史记录。')}`);
|
|
331
|
+
console.log(` ${c.gray('运行 devassist start 后,文件变更的检查/分析/修复记录会自动保存到这里。')}\n`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Parse limit from args
|
|
336
|
+
let limit = 20;
|
|
337
|
+
const idx = args.indexOf('--watch-history');
|
|
338
|
+
if (idx >= 0 && args[idx + 1] && /^\d+$/.test(args[idx + 1])) {
|
|
339
|
+
limit = parseInt(args[idx + 1]);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Read JSONL log
|
|
343
|
+
const lines = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
344
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
|
|
345
|
+
|
|
346
|
+
if (entries.length === 0) {
|
|
347
|
+
console.log(` ${c.yellow('暂无监控历史记录。')}\n`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Show recent entries (newest first)
|
|
352
|
+
const recent = entries.slice(-limit).reverse();
|
|
353
|
+
console.log(` ${c.gray(`共 ${entries.length} 条记录,显示最近 ${recent.length} 条:`)}\n`);
|
|
354
|
+
|
|
355
|
+
// Summary stats
|
|
356
|
+
const totalEvents = entries.length;
|
|
357
|
+
const totalFindings = entries.reduce((s, e) => s + (e.total || 0), 0);
|
|
358
|
+
const totalBlocks = entries.reduce((s, e) => s + (e.blocks || 0), 0);
|
|
359
|
+
const totalFixed = entries.reduce((s, e) => s + (e.fixed || 0), 0);
|
|
360
|
+
const aiEvents = entries.filter(e => e.aiUsed).length;
|
|
361
|
+
|
|
362
|
+
console.log(` ${c.bold('汇总统计:')}`);
|
|
363
|
+
console.log(` 监控事件:${totalEvents} 次`);
|
|
364
|
+
console.log(` 发现问题:${totalFindings} 个(其中 ${c.red(totalBlocks + ' 阻断')})`);
|
|
365
|
+
console.log(` 自动修复:${c.green(totalFixed + ' 处')}`);
|
|
366
|
+
console.log(` AI 分析:${aiEvents} 次\n`);
|
|
367
|
+
|
|
368
|
+
// Detail table
|
|
369
|
+
console.log(` ${c.bold('最近事件:')}\n`);
|
|
370
|
+
console.log(` ${c.gray('时间'.padEnd(20))} ${c.gray('文件'.padEnd(30))} ${c.gray('问题'.padEnd(8))} ${c.gray('修复'.padEnd(6))} ${c.gray('引擎')}`);
|
|
371
|
+
console.log(` ${'─'.repeat(90)}`);
|
|
372
|
+
|
|
373
|
+
for (const e of recent) {
|
|
374
|
+
const time = new Date(e.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
375
|
+
const file = (e.file || '').length > 28 ? '...' + (e.file || '').slice(-25) : (e.file || '');
|
|
376
|
+
const issues = e.total > 0 ? c.yellow(String(e.total)) : c.green('0');
|
|
377
|
+
const blocks = e.blocks > 0 ? c.red(`(${e.blocks}阻断)`) : '';
|
|
378
|
+
const fixed = e.fixed > 0 ? c.green(String(e.fixed)) : '-';
|
|
379
|
+
const engines = e.engines || '-';
|
|
380
|
+
|
|
381
|
+
console.log(` ${time.padEnd(20)} ${file.padEnd(30)} ${issues} ${blocks}`.padEnd(58) + ` ${fixed.toString().padEnd(6)} ${engines}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Latest report detail
|
|
385
|
+
const latestPath = path.join(projectRoot, '.devassist', 'reports', 'latest.json');
|
|
386
|
+
if (fs.existsSync(latestPath)) {
|
|
387
|
+
console.log(`\n ${c.gray('最近一次完整报告:')} ${latestPath}`);
|
|
388
|
+
console.log(` ${c.gray('历史报告目录:')} ${historyDir}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Watch files with most issues
|
|
392
|
+
const fileStats = {};
|
|
393
|
+
for (const e of entries) {
|
|
394
|
+
if (!fileStats[e.file]) fileStats[e.file] = { count: 0, issues: 0, blocks: 0, fixed: 0 };
|
|
395
|
+
fileStats[e.file].count++;
|
|
396
|
+
fileStats[e.file].issues += e.total || 0;
|
|
397
|
+
fileStats[e.file].blocks += e.blocks || 0;
|
|
398
|
+
fileStats[e.file].fixed += e.fixed || 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const hotFiles = Object.entries(fileStats)
|
|
402
|
+
.sort((a, b) => b[1].issues - a[1].issues)
|
|
403
|
+
.filter(([, s]) => s.issues > 0)
|
|
404
|
+
.slice(0, 5);
|
|
405
|
+
|
|
406
|
+
if (hotFiles.length > 0) {
|
|
407
|
+
console.log(`\n ${c.bold('问题热点文件 TOP 5:')}\n`);
|
|
408
|
+
for (const [file, s] of hotFiles) {
|
|
409
|
+
console.log(` ${file} — ${s.issues} 个问题,${s.blocks} 阻断,${s.fixed} 已修复(${s.count} 次变更)`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log('');
|
|
414
|
+
}
|
|
@@ -171,9 +171,6 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
171
171
|
// Apply inline ignores
|
|
172
172
|
allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
|
|
173
173
|
|
|
174
|
-
// Save findings for ai --analyze
|
|
175
|
-
saveWatchFindings(projectRoot, allFindings);
|
|
176
|
-
|
|
177
174
|
// Print header
|
|
178
175
|
console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
|
|
179
176
|
|
|
@@ -270,13 +267,37 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
270
267
|
console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
|
|
271
268
|
|
|
272
269
|
// --- Auto Fix (if enabled) ---
|
|
270
|
+
let fixResult = null;
|
|
273
271
|
if (autoFix && suspectFindings.length > 0) {
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
console.log(` ${c.green('自动修复')} ${
|
|
272
|
+
fixResult = await autoFixFile(filePath, content, projectRoot, c);
|
|
273
|
+
if (fixResult.count > 0) {
|
|
274
|
+
console.log(` ${c.green('自动修复')} ${fixResult.count} 处问题`);
|
|
275
|
+
for (const d of fixResult.details) {
|
|
276
|
+
const typeLabel = d.type === 'fetch-catch' ? 'fetch 添加 .catch()' : '添加 await';
|
|
277
|
+
console.log(` ${c.gray(`第${d.line}行`)} ${typeLabel}`);
|
|
278
|
+
}
|
|
277
279
|
}
|
|
278
280
|
}
|
|
279
281
|
|
|
282
|
+
// --- Save comprehensive report for retrospective ---
|
|
283
|
+
saveWatchReport(projectRoot, {
|
|
284
|
+
timestamp: new Date().toISOString(),
|
|
285
|
+
file: relPath,
|
|
286
|
+
findings: finalFindings,
|
|
287
|
+
aiAnalysisUsed: useAI && readyEngines.length > 0 && suspectFindings.length > 0,
|
|
288
|
+
compareMode: useCompare && readyEngines.length >= 2,
|
|
289
|
+
engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
|
|
290
|
+
fixes: fixResult ? fixResult.details : [],
|
|
291
|
+
fixBackupPath: fixResult ? fixResult.backupPath : null,
|
|
292
|
+
stats: {
|
|
293
|
+
total: finalFindings.length,
|
|
294
|
+
blocks: blocks.length,
|
|
295
|
+
warnings: warns.length,
|
|
296
|
+
infos: infos.length,
|
|
297
|
+
fixed: fixResult ? fixResult.count : 0,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
280
301
|
} finally {
|
|
281
302
|
analyzingFiles.delete(filePath);
|
|
282
303
|
}
|
|
@@ -346,7 +367,7 @@ async function autoFixFile(filePath, content, projectRoot, c) {
|
|
|
346
367
|
const trimmed = line.trimEnd();
|
|
347
368
|
if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
|
|
348
369
|
const fixedLine = trimmed.replace(/\)\s*$/, ").catch(err => console.error('fetch error:', err))");
|
|
349
|
-
changes.push({ line: i
|
|
370
|
+
changes.push({ line: i + 1, type: 'fetch-catch', before: line, after: fixedLine });
|
|
350
371
|
fixCount++;
|
|
351
372
|
continue;
|
|
352
373
|
}
|
|
@@ -366,7 +387,7 @@ async function autoFixFile(filePath, content, projectRoot, c) {
|
|
|
366
387
|
'await $1('
|
|
367
388
|
);
|
|
368
389
|
if (fixedLine !== line) {
|
|
369
|
-
changes.push({ line: i
|
|
390
|
+
changes.push({ line: i + 1, type: 'add-await', before: line, after: fixedLine });
|
|
370
391
|
fixCount++;
|
|
371
392
|
continue;
|
|
372
393
|
}
|
|
@@ -374,11 +395,11 @@ async function autoFixFile(filePath, content, projectRoot, c) {
|
|
|
374
395
|
}
|
|
375
396
|
}
|
|
376
397
|
|
|
377
|
-
if (fixCount === 0) return 0;
|
|
398
|
+
if (fixCount === 0) return { count: 0, details: [] };
|
|
378
399
|
|
|
379
400
|
// Apply changes
|
|
380
401
|
for (const ch of changes) {
|
|
381
|
-
lines[ch.line] = ch.
|
|
402
|
+
lines[ch.line - 1] = ch.after;
|
|
382
403
|
}
|
|
383
404
|
const newContent = lines.join('\n');
|
|
384
405
|
|
|
@@ -391,22 +412,60 @@ async function autoFixFile(filePath, content, projectRoot, c) {
|
|
|
391
412
|
// Write fixed content
|
|
392
413
|
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
393
414
|
|
|
394
|
-
return fixCount;
|
|
415
|
+
return { count: fixCount, details: changes, backupPath };
|
|
395
416
|
}
|
|
396
417
|
|
|
397
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Save comprehensive watch report for retrospective.
|
|
420
|
+
* - latest.json: always overwritten with the latest event
|
|
421
|
+
* - history/watch-<timestamp>.json: per-event detail, kept for 90 days
|
|
422
|
+
* - history/watch-log.jsonl: append-only summary log for quick querying
|
|
423
|
+
*/
|
|
424
|
+
function saveWatchReport(projectRoot, report) {
|
|
398
425
|
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
399
426
|
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
427
|
+
|
|
428
|
+
// 1. Save latest.json (full detail, always latest)
|
|
429
|
+
const latestPath = path.join(reportDir, 'latest.json');
|
|
430
|
+
fs.writeFileSync(latestPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
431
|
+
|
|
432
|
+
// 2. Save timestamped history
|
|
433
|
+
const historyDir = path.join(projectRoot, '.devassist', 'history', 'watch');
|
|
434
|
+
if (!fs.existsSync(historyDir)) fs.mkdirSync(historyDir, { recursive: true });
|
|
435
|
+
|
|
436
|
+
const ts = new Date();
|
|
437
|
+
const tsStr = ts.toISOString().replace(/[:.]/g, '-');
|
|
438
|
+
const historyPath = path.join(historyDir, `watch-${tsStr}.json`);
|
|
439
|
+
fs.writeFileSync(historyPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
440
|
+
|
|
441
|
+
// 3. Append summary to JSONL log
|
|
442
|
+
const logPath = path.join(historyDir, 'watch-log.jsonl');
|
|
443
|
+
const summary = {
|
|
444
|
+
timestamp: report.timestamp,
|
|
445
|
+
file: report.file,
|
|
446
|
+
total: report.stats.total,
|
|
447
|
+
blocks: report.stats.blocks,
|
|
448
|
+
warnings: report.stats.warnings,
|
|
449
|
+
infos: report.stats.infos,
|
|
450
|
+
fixed: report.stats.fixed,
|
|
451
|
+
aiUsed: report.aiAnalysisUsed,
|
|
452
|
+
compare: report.compareMode,
|
|
453
|
+
engines: report.engines.map(e => e.provider).join('+'),
|
|
454
|
+
};
|
|
455
|
+
fs.appendFileSync(logPath, JSON.stringify(summary) + '\n', 'utf-8');
|
|
456
|
+
|
|
457
|
+
// 4. Auto-cleanup: remove history files older than 90 days
|
|
458
|
+
try {
|
|
459
|
+
const files = fs.readdirSync(historyDir);
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
const maxAge = 90 * 24 * 60 * 60 * 1000; // 90 days in ms
|
|
462
|
+
for (const f of files) {
|
|
463
|
+
if (!f.startsWith('watch-') || !f.endsWith('.json')) continue;
|
|
464
|
+
const filePath = path.join(historyDir, f);
|
|
465
|
+
const stat = fs.statSync(filePath);
|
|
466
|
+
if (now - stat.mtime.getTime() > maxAge) {
|
|
467
|
+
fs.unlinkSync(filePath);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (_) {}
|
|
412
471
|
}
|