devassist-agent 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/LICENSE +21 -0
- package/README.md +467 -0
- package/bin/devassist.js +220 -0
- package/package.json +44 -0
- package/src/ai/adapter.js +464 -0
- package/src/ai/providers/claude.js +80 -0
- package/src/ai/providers/hunyuan.js +87 -0
- package/src/ai/providers/openai.js +74 -0
- package/src/ai/providers/qwen.js +81 -0
- package/src/cli/commands/ai.js +944 -0
- package/src/cli/commands/ask.js +79 -0
- package/src/cli/commands/backup.js +30 -0
- package/src/cli/commands/check.js +327 -0
- package/src/cli/commands/clean.js +130 -0
- package/src/cli/commands/comparison-report.js +326 -0
- package/src/cli/commands/convention.js +91 -0
- package/src/cli/commands/debt.js +49 -0
- package/src/cli/commands/deploy.js +88 -0
- package/src/cli/commands/diff.js +193 -0
- package/src/cli/commands/doctor.js +186 -0
- package/src/cli/commands/fix.js +195 -0
- package/src/cli/commands/init.js +431 -0
- package/src/cli/commands/inject.js +254 -0
- package/src/cli/commands/report.js +310 -0
- package/src/cli/commands/restore.js +78 -0
- package/src/cli/commands/schema.js +93 -0
- package/src/cli/commands/watch.js +212 -0
- package/src/cli/shared/code-context.js +51 -0
- package/src/cli/shared/config-loader.js +89 -0
- package/src/cli/shared/file-collector.js +116 -0
- package/src/cli/shared/inline-ignore.js +142 -0
- package/src/cli/shared/watch-list.js +281 -0
- package/src/core/event-bus.js +83 -0
- package/src/core/fsm.js +103 -0
- package/src/core/logger.js +54 -0
- package/src/core/rule-engine.js +117 -0
- package/src/index.js +64 -0
- package/src/modules/dev-time/arch-risk-assessor.js +250 -0
- package/src/modules/dev-time/code-quality-guard.js +1340 -0
- package/src/modules/dev-time/convention-store.js +201 -0
- package/src/modules/dev-time/impact-analyzer.js +292 -0
- package/src/modules/dev-time/pre-deploy-guard.js +284 -0
- package/src/modules/dev-time/schema-registry.js +284 -0
- package/src/modules/dev-time/tech-debt-tracker.js +225 -0
- package/src/modules/dev-time/version-manager.js +280 -0
- package/src/modules/runtime/channel-agent.js +404 -0
- package/src/modules/runtime/channel-middleware.js +316 -0
- package/src/modules/runtime/index.js +64 -0
- package/src/modules/runtime/infrastructure-guard.js +582 -0
- package/src/modules/runtime/notification-center.js +443 -0
- package/src/modules/runtime/schema-gatekeeper.js +664 -0
- package/src/modules/runtime/tool-registry.js +329 -0
- package/templates/ci/github-actions.yml +60 -0
- package/templates/ci/gitlab-ci.yml +30 -0
- package/templates/ci/pre-commit-hook.sh +18 -0
- package/templates/git-hooks/pre-commit +61 -0
- package/tests/run-all.js +434 -0
- package/tests/test-layer2.js +461 -0
- package/tests/test-new-rules.js +157 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist ask <question> - Ask AI a question about your codebase
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const ai = require('../../ai/adapter');
|
|
8
|
+
const qwen = require('../../ai/providers/qwen');
|
|
9
|
+
const openai = require('../../ai/providers/openai');
|
|
10
|
+
const claude = require('../../ai/providers/claude');
|
|
11
|
+
const hunyuan = require('../../ai/providers/hunyuan');
|
|
12
|
+
const { ConventionStore } = require('../../modules/dev-time/convention-store');
|
|
13
|
+
|
|
14
|
+
ai.registerProvider('qwen', qwen);
|
|
15
|
+
ai.registerProvider('openai', openai);
|
|
16
|
+
ai.registerProvider('claude', claude);
|
|
17
|
+
ai.registerProvider('hunyuan', hunyuan);
|
|
18
|
+
|
|
19
|
+
module.exports = async function ask(projectRoot, args, ui) {
|
|
20
|
+
const { printHeader, c } = ui;
|
|
21
|
+
printHeader('Ask AI');
|
|
22
|
+
|
|
23
|
+
const question = args.filter(a => !a.startsWith('--')).join(' ');
|
|
24
|
+
if (!question) {
|
|
25
|
+
console.log(` Usage: devassist ask ${c.gray('<your question>')}\n`);
|
|
26
|
+
console.log(` ${c.gray('Example:')} devassist ask "Why is my API returning data.items instead of data.list?"\n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Load AI config
|
|
31
|
+
const configPath = path.join(projectRoot, '.devassist', 'ai-config.json');
|
|
32
|
+
let config = null;
|
|
33
|
+
if (fs.existsSync(configPath)) {
|
|
34
|
+
try {
|
|
35
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
36
|
+
// Override with env vars
|
|
37
|
+
if (!config.apiKey) {
|
|
38
|
+
const envMap = { qwen: 'DASHSCOPE_API_KEY', openai: 'OPENAI_API_KEY', claude: 'ANTHROPIC_API_KEY', hunyuan: 'HUNYUAN_API_KEY' };
|
|
39
|
+
const envVar = envMap[config.provider];
|
|
40
|
+
if (envVar && process.env[envVar]) {
|
|
41
|
+
config.apiKey = process.env[envVar];
|
|
42
|
+
config.enabled = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!config || !config.enabled || !config.provider) {
|
|
49
|
+
console.log(` ${c.yellow('AI 尚未配置。')}\n`);
|
|
50
|
+
console.log(` ${c.gray('启用步骤:')}`);
|
|
51
|
+
console.log(` 1. 编辑 .devassist/ai-config.json`);
|
|
52
|
+
console.log(` 2. 设置 "provider": "qwen"(或 "openai" / "claude" / "hunyuan")`);
|
|
53
|
+
console.log(` 3. 设置 "apiKey": "你的API密钥"`);
|
|
54
|
+
console.log(` 4. 设置 "enabled": true`);
|
|
55
|
+
console.log(`\n ${c.gray('或设置环境变量:')}`);
|
|
56
|
+
console.log(` ${c.bold('DASHSCOPE_API_KEY')} 通义千问`);
|
|
57
|
+
console.log(` ${c.bold('OPENAI_API_KEY')} OpenAI`);
|
|
58
|
+
console.log(` ${c.bold('ANTHROPIC_API_KEY')} Claude`);
|
|
59
|
+
console.log(` ${c.bold('HUNYUAN_API_KEY')} 腾讯混元\n`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load conventions as context
|
|
64
|
+
const conventionStore = new ConventionStore(projectRoot);
|
|
65
|
+
const conventions = conventionStore.getPromptBlock({});
|
|
66
|
+
|
|
67
|
+
console.log(` ${c.gray('Question:')} ${question}`);
|
|
68
|
+
console.log(` ${c.gray('Provider:')} ${config.provider} (${config.model || 'default'})\n`);
|
|
69
|
+
console.log(` ${c.gray('Thinking...')}\n`);
|
|
70
|
+
|
|
71
|
+
const result = await ai.ask(config, question, { conventions });
|
|
72
|
+
|
|
73
|
+
if (result.unavailable || result.error) {
|
|
74
|
+
console.log(` ${c.red('AI error:')} ${result.content}\n`);
|
|
75
|
+
} else {
|
|
76
|
+
console.log(` ${result.content}\n`);
|
|
77
|
+
if (result.model) console.log(` ${c.gray('(model: ' + result.model + ')')}\n`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist backup - Create incremental backup
|
|
3
|
+
* devassist restore - Restore from backup (non-destructive)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { VersionManager } = require('../../modules/dev-time/version-manager');
|
|
7
|
+
|
|
8
|
+
module.exports = function backup(projectRoot, args, ui) {
|
|
9
|
+
const { printHeader, c } = ui;
|
|
10
|
+
printHeader('创建备份');
|
|
11
|
+
|
|
12
|
+
const vm = new VersionManager(projectRoot);
|
|
13
|
+
const label = args.find(a => !a.startsWith('--')) || '手动备份';
|
|
14
|
+
|
|
15
|
+
const manifest = vm.backup(label);
|
|
16
|
+
console.log(` ${c.green('备份已创建:')} ${manifest.versionId}`);
|
|
17
|
+
console.log(` ${c.bold('标签:')} ${manifest.label}`);
|
|
18
|
+
console.log(` ${c.bold('文件:')} 共 ${manifest.files.length} 个`);
|
|
19
|
+
console.log(` ${manifest.files.filter(f => f.status === 'new').length} 新增`);
|
|
20
|
+
console.log(` ${manifest.files.filter(f => f.status === 'changed').length} 变更`);
|
|
21
|
+
console.log(` ${manifest.files.filter(f => f.status === 'unchanged').length} 未变\n`);
|
|
22
|
+
|
|
23
|
+
// Also list available versions
|
|
24
|
+
const versions = vm.listVersions();
|
|
25
|
+
console.log(` ${c.gray('可用备份:')}`);
|
|
26
|
+
for (const v of versions.slice(-5)) {
|
|
27
|
+
console.log(` ${c.blue(v.versionId)} ${c.gray(v.label)} ${c.gray('(' + v.stats.total + ' 个文件)')}`);
|
|
28
|
+
}
|
|
29
|
+
console.log();
|
|
30
|
+
};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist check - Run all dev-time checks
|
|
3
|
+
*
|
|
4
|
+
* Executes:
|
|
5
|
+
* 1. CodeQualityGuard rules (16 rules: null safety, race conditions, duplicates, etc.)
|
|
6
|
+
* 2. ConventionStore checks (naming, patterns)
|
|
7
|
+
* 3. ImpactAnalyzer (if git diff available, check change impact)
|
|
8
|
+
* 4. ArchRiskAssessor (complexity, circular deps, injection)
|
|
9
|
+
* 5. SchemaRegistry drift detection (data.list vs data.items)
|
|
10
|
+
* 6. TechDebtTracker scan (file size, coupling, duplication)
|
|
11
|
+
*
|
|
12
|
+
* Post-processing:
|
|
13
|
+
* - Apply .devassist/config.json rule overrides (enable/disable/severity)
|
|
14
|
+
* - Filter out findings covered by inline ignore comments
|
|
15
|
+
* - Enrich findings with code context for AI analysis
|
|
16
|
+
*
|
|
17
|
+
* If --ai flag is set, sends suspect findings to AI for deep analysis.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// Core
|
|
24
|
+
const { engine } = require('../../core/rule-engine');
|
|
25
|
+
|
|
26
|
+
// Shared utilities
|
|
27
|
+
const { collectFileContents } = require('../shared/file-collector');
|
|
28
|
+
const { loadConfig, applyConfigToFindings } = require('../shared/config-loader');
|
|
29
|
+
const { filterIgnored } = require('../shared/inline-ignore');
|
|
30
|
+
const { enrichWithContext } = require('../shared/code-context');
|
|
31
|
+
|
|
32
|
+
// Modules
|
|
33
|
+
const { ConventionStore } = require('../../modules/dev-time/convention-store');
|
|
34
|
+
const { codeQualityRules } = require('../../modules/dev-time/code-quality-guard');
|
|
35
|
+
const { ImpactAnalyzer } = require('../../modules/dev-time/impact-analyzer');
|
|
36
|
+
const { ArchRiskAssessor } = require('../../modules/dev-time/arch-risk-assessor');
|
|
37
|
+
const { TechDebtTracker } = require('../../modules/dev-time/tech-debt-tracker');
|
|
38
|
+
const { SchemaRegistry } = require('../../modules/dev-time/schema-registry');
|
|
39
|
+
|
|
40
|
+
// AI
|
|
41
|
+
const ai = require('../../ai/adapter');
|
|
42
|
+
const qwen = require('../../ai/providers/qwen');
|
|
43
|
+
const openai = require('../../ai/providers/openai');
|
|
44
|
+
const claude = require('../../ai/providers/claude');
|
|
45
|
+
const hunyuan = require('../../ai/providers/hunyuan');
|
|
46
|
+
const { loadAIConfig, PROVIDER_INFO, computeAgreementStats } = require('./ai');
|
|
47
|
+
|
|
48
|
+
// Register AI providers
|
|
49
|
+
ai.registerProvider('qwen', qwen);
|
|
50
|
+
ai.registerProvider('openai', openai);
|
|
51
|
+
ai.registerProvider('claude', claude);
|
|
52
|
+
ai.registerProvider('hunyuan', hunyuan);
|
|
53
|
+
|
|
54
|
+
module.exports = async function check(projectRoot, args, ui) {
|
|
55
|
+
const { printHeader, printFinding, printStats, c } = ui;
|
|
56
|
+
const isPreCommit = args.includes('--pre-commit');
|
|
57
|
+
const useAI = args.includes('--ai');
|
|
58
|
+
const useCompare = args.includes('--compare');
|
|
59
|
+
const jsonOutput = args.includes('--json');
|
|
60
|
+
|
|
61
|
+
printHeader('运行全部检查');
|
|
62
|
+
|
|
63
|
+
// Load project config
|
|
64
|
+
const config = loadConfig(projectRoot);
|
|
65
|
+
|
|
66
|
+
// Collect all source files
|
|
67
|
+
const fileContents = collectFileContents(projectRoot);
|
|
68
|
+
if (fileContents.length === 0) {
|
|
69
|
+
console.log(` ${c.yellow('未找到源文件。')} 请确认项目目录是否正确。\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ctx = {
|
|
74
|
+
files: fileContents,
|
|
75
|
+
projectRoot,
|
|
76
|
+
config,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 1. Register and run CodeQualityGuard rules
|
|
80
|
+
engine.registerBatch(codeQualityRules);
|
|
81
|
+
|
|
82
|
+
// 2. Load conventions and add convention rules
|
|
83
|
+
const conventionStore = new ConventionStore(projectRoot);
|
|
84
|
+
engine.register({
|
|
85
|
+
id: 'convention-check',
|
|
86
|
+
severity: 'warn',
|
|
87
|
+
category: 'convention',
|
|
88
|
+
check(ctx) {
|
|
89
|
+
const allFindings = [];
|
|
90
|
+
for (const file of ctx.files) {
|
|
91
|
+
const findings = conventionStore.check(file.path, file.content);
|
|
92
|
+
allFindings.push(...findings);
|
|
93
|
+
}
|
|
94
|
+
return allFindings.length > 0 ? allFindings : null;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Run rule engine
|
|
99
|
+
const { findings, stats } = engine.run(ctx);
|
|
100
|
+
|
|
101
|
+
// 3. Impact analysis (if git diff available)
|
|
102
|
+
let impactFindings = [];
|
|
103
|
+
if (!isPreCommit) {
|
|
104
|
+
try {
|
|
105
|
+
const { execSync } = require('child_process');
|
|
106
|
+
const diff = execSync('git diff --name-only HEAD 2>/dev/null', { cwd: projectRoot, encoding: 'utf-8' }).trim();
|
|
107
|
+
if (diff) {
|
|
108
|
+
const impactAnalyzer = new ImpactAnalyzer(projectRoot);
|
|
109
|
+
impactAnalyzer.buildGraph();
|
|
110
|
+
const changedFiles = diff.split('\n').filter(f => f);
|
|
111
|
+
for (const file of changedFiles) {
|
|
112
|
+
const fullPath = path.join(projectRoot, file);
|
|
113
|
+
if (fs.existsSync(fullPath)) {
|
|
114
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
115
|
+
const result = impactAnalyzer.analyzeChange(fullPath, content);
|
|
116
|
+
impactFindings.push(...result.findings);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {} // Not a git repo or no changes
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. Architecture risk assessment
|
|
124
|
+
const archAssessor = new ArchRiskAssessor(projectRoot);
|
|
125
|
+
const archFindings = [];
|
|
126
|
+
for (const file of fileContents) {
|
|
127
|
+
const findings = archAssessor.analyzeFile(file.path, file.content);
|
|
128
|
+
archFindings.push(...findings);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. SchemaRegistry drift detection
|
|
132
|
+
const schemaRegistry = new SchemaRegistry(projectRoot);
|
|
133
|
+
const schemaFindings = schemaRegistry.detectDrift ? schemaRegistry.detectDrift(fileContents) : [];
|
|
134
|
+
|
|
135
|
+
// 6. TechDebtTracker scan
|
|
136
|
+
let debtFindings = [];
|
|
137
|
+
try {
|
|
138
|
+
const debtTracker = new TechDebtTracker(projectRoot);
|
|
139
|
+
const debtBoard = debtTracker.scan();
|
|
140
|
+
if (debtBoard && debtBoard.items) {
|
|
141
|
+
// Convert high-priority debt items to findings
|
|
142
|
+
debtFindings = debtBoard.items
|
|
143
|
+
.filter(item => item.severity === 'high')
|
|
144
|
+
.slice(0, 10) // limit to top 10
|
|
145
|
+
.map(item => ({
|
|
146
|
+
severity: 'warn',
|
|
147
|
+
message: `技术债务:${item.description || item.type || '未知问题'}`,
|
|
148
|
+
file: item.file || '(unknown)',
|
|
149
|
+
line: item.line || 1,
|
|
150
|
+
suggestion: `优先级 ${item.priority ? item.priority.toFixed(2) : '?'},预估修复工时 ${item.effort || '?'}。运行 devassist debt --scan 查看详情。`,
|
|
151
|
+
ruleId: 'debt-tracker',
|
|
152
|
+
category: 'tech-debt',
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
} catch (_) {}
|
|
156
|
+
|
|
157
|
+
// Merge all findings
|
|
158
|
+
let allFindings = [...findings, ...impactFindings, ...archFindings, ...schemaFindings, ...debtFindings];
|
|
159
|
+
|
|
160
|
+
// Post-processing: apply config-based rule overrides
|
|
161
|
+
allFindings = applyConfigToFindings(config, allFindings);
|
|
162
|
+
|
|
163
|
+
// Post-processing: filter inline ignore comments
|
|
164
|
+
allFindings = filterIgnored(allFindings, fileContents);
|
|
165
|
+
|
|
166
|
+
// Enrich with code context for AI analysis
|
|
167
|
+
allFindings = enrichWithContext(allFindings, fileContents);
|
|
168
|
+
|
|
169
|
+
// 7. AI analysis (optional)
|
|
170
|
+
let finalFindings = allFindings;
|
|
171
|
+
let aiConfig = null;
|
|
172
|
+
if (useAI || useCompare || (config.ai && config.ai.autoAnalyze)) {
|
|
173
|
+
aiConfig = loadAIConfig(projectRoot);
|
|
174
|
+
if (aiConfig && aiConfig.engines && aiConfig.engines.length > 0) {
|
|
175
|
+
const readyEngines = aiConfig.engines.filter(e => e.enabled && e.apiKey);
|
|
176
|
+
if (readyEngines.length > 0) {
|
|
177
|
+
const aiContext = { conventions: conventionStore.getRelevant({}), projectInfo: { root: projectRoot } };
|
|
178
|
+
|
|
179
|
+
if (useCompare && readyEngines.length >= 2) {
|
|
180
|
+
// Multi-engine comparison mode
|
|
181
|
+
console.log(` ${c.cyan(`${readyEngines.length} 引擎对比模式:`)}${readyEngines.map(e => e.provider).join(' + ')}`);
|
|
182
|
+
console.log(` ${c.gray('正在并行分析...')}\n`);
|
|
183
|
+
finalFindings = await ai.multiAnalyze(readyEngines, allFindings, aiContext);
|
|
184
|
+
} else {
|
|
185
|
+
// Single engine mode (use primary/first engine)
|
|
186
|
+
console.log(` ${c.cyan('AI 分析模式:')}${readyEngines[0].provider}`);
|
|
187
|
+
finalFindings = await ai.analyze(readyEngines[0], allFindings, aiContext);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
console.log(` ${c.gray('(AI 引擎未就绪。请在 .devassist/ai-config.json 中配置 API Key)')}\n`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
console.log(` ${c.gray('(AI 分析已禁用。请在 .devassist/ai-config.json 中配置)')}\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Output
|
|
198
|
+
if (jsonOutput) {
|
|
199
|
+
console.log(JSON.stringify({ findings: finalFindings, stats }, null, 2));
|
|
200
|
+
saveFindings(projectRoot, finalFindings, stats);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Save findings for ai --analyze (even in non-json mode)
|
|
205
|
+
saveFindings(projectRoot, finalFindings, stats);
|
|
206
|
+
|
|
207
|
+
// Save check history
|
|
208
|
+
saveHistory(projectRoot, finalFindings);
|
|
209
|
+
|
|
210
|
+
// Generate comparison HTML report if in compare mode
|
|
211
|
+
if (useCompare && finalFindings.some(f => f.aiAnalyses && f.aiAnalyses.length >= 2)) {
|
|
212
|
+
saveComparisonReport(projectRoot, finalFindings, aiConfig, c);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Print findings
|
|
216
|
+
if (finalFindings.length === 0) {
|
|
217
|
+
console.log(` ${c.green('全部检查通过!')} 未发现问题。\n`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const f of finalFindings) {
|
|
222
|
+
printFinding(f);
|
|
223
|
+
}
|
|
224
|
+
printStats({
|
|
225
|
+
...stats,
|
|
226
|
+
blocks: finalFindings.filter(f => f.severity === 'block').length,
|
|
227
|
+
warnings: finalFindings.filter(f => f.severity === 'warn').length,
|
|
228
|
+
infos: finalFindings.filter(f => f.severity === 'info').length,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Exit code for pre-commit
|
|
232
|
+
const hasBlocks = finalFindings.some(f => f.severity === 'block');
|
|
233
|
+
if (hasBlocks && isPreCommit) {
|
|
234
|
+
console.log(` ${c.red('提交已阻断。')} 请修复 BLOCK 级别问题,或使用 ${c.bold('git commit --no-verify')} 跳过检查。\n`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
function saveFindings(projectRoot, findings, stats) {
|
|
240
|
+
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
241
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
242
|
+
const reportPath = path.join(reportDir, 'latest.json');
|
|
243
|
+
fs.writeFileSync(reportPath, JSON.stringify({
|
|
244
|
+
generatedAt: new Date().toISOString(),
|
|
245
|
+
findings,
|
|
246
|
+
stats,
|
|
247
|
+
}, null, 2), 'utf-8');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function saveHistory(projectRoot, findings) {
|
|
251
|
+
const historyDir = path.join(projectRoot, '.devassist', 'history');
|
|
252
|
+
if (!fs.existsSync(historyDir)) fs.mkdirSync(historyDir, { recursive: true });
|
|
253
|
+
const now = new Date();
|
|
254
|
+
const dateStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
|
|
255
|
+
const entry = {
|
|
256
|
+
date: now.toISOString(),
|
|
257
|
+
blocks: findings.filter(f => f.severity === 'block').length,
|
|
258
|
+
warnings: findings.filter(f => f.severity === 'warn').length,
|
|
259
|
+
infos: findings.filter(f => f.severity === 'info').length,
|
|
260
|
+
total: findings.length,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Append to daily history file
|
|
264
|
+
const historyFile = path.join(historyDir, `${dateStr}.json`);
|
|
265
|
+
let entries = [];
|
|
266
|
+
if (fs.existsSync(historyFile)) {
|
|
267
|
+
try { entries = JSON.parse(fs.readFileSync(historyFile, 'utf-8')); } catch (_) {}
|
|
268
|
+
}
|
|
269
|
+
entries.push(entry);
|
|
270
|
+
fs.writeFileSync(historyFile, JSON.stringify(entries, null, 2), 'utf-8');
|
|
271
|
+
|
|
272
|
+
// Clean up history older than 90 days
|
|
273
|
+
try {
|
|
274
|
+
const files = fs.readdirSync(historyDir);
|
|
275
|
+
const cutoff = new Date();
|
|
276
|
+
cutoff.setDate(cutoff.getDate() - 90);
|
|
277
|
+
for (const f of files) {
|
|
278
|
+
if (!f.endsWith('.json')) continue;
|
|
279
|
+
const fileDate = new Date(f.replace('.json', ''));
|
|
280
|
+
if (fileDate < cutoff) {
|
|
281
|
+
fs.unlinkSync(path.join(historyDir, f));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (_) {}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function saveComparisonReport(projectRoot, findings, aiConfig, c) {
|
|
288
|
+
const { generateComparisonHTML } = require('./comparison-report');
|
|
289
|
+
const watchList = require('../shared/watch-list');
|
|
290
|
+
const readyEngines = aiConfig.engines.filter(e => e.enabled && e.apiKey);
|
|
291
|
+
const stats = computeAgreementStats(findings, readyEngines.length);
|
|
292
|
+
|
|
293
|
+
// Auto-add disagreement findings to watch list
|
|
294
|
+
const deferredCount = watchList.addDeferredFindings(projectRoot, findings,
|
|
295
|
+
'待观察:引擎判断不一致,等运行时验证');
|
|
296
|
+
|
|
297
|
+
const html = generateComparisonHTML({
|
|
298
|
+
projectRoot: path.basename(projectRoot),
|
|
299
|
+
timestamp: new Date().toISOString(),
|
|
300
|
+
engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
|
|
301
|
+
stats,
|
|
302
|
+
findings,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
306
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
307
|
+
const htmlPath = path.join(reportDir, 'ai-comparison.html');
|
|
308
|
+
fs.writeFileSync(htmlPath, html, 'utf-8');
|
|
309
|
+
|
|
310
|
+
// Also save JSON
|
|
311
|
+
const jsonPath = path.join(reportDir, 'ai-comparison.json');
|
|
312
|
+
fs.writeFileSync(jsonPath, JSON.stringify({
|
|
313
|
+
analyzedAt: new Date().toISOString(),
|
|
314
|
+
engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
|
|
315
|
+
stats,
|
|
316
|
+
findings,
|
|
317
|
+
}, null, 2), 'utf-8');
|
|
318
|
+
|
|
319
|
+
console.log(` ${c.green('多引擎对比报告已生成:')}`);
|
|
320
|
+
console.log(` HTML: .devassist/reports/ai-comparison.html`);
|
|
321
|
+
console.log(` JSON: .devassist/reports/ai-comparison.json`);
|
|
322
|
+
if (deferredCount > 0) {
|
|
323
|
+
console.log(` ${c.yellow(`⏳ ${deferredCount} 个判断不一致的发现已标记为待观察(.devassist/watch-list.json)`)}`);
|
|
324
|
+
console.log(` ${c.gray('暂不修复,等实际运行验证后再处理。')}`);
|
|
325
|
+
}
|
|
326
|
+
console.log('');
|
|
327
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist clean - Clean up old reports, backups, and history
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* devassist clean # Clean items older than 30 days
|
|
6
|
+
* devassist clean --days 7 # Clean items older than 7 days
|
|
7
|
+
* devassist clean --all # Clean all except latest
|
|
8
|
+
* devassist clean --dry-run # Show what would be cleaned without deleting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
module.exports = function clean(projectRoot, args, ui) {
|
|
15
|
+
const { printHeader, c } = ui;
|
|
16
|
+
printHeader('清理旧文件');
|
|
17
|
+
|
|
18
|
+
const dryRun = args.includes('--dry-run');
|
|
19
|
+
const cleanAll = args.includes('--all');
|
|
20
|
+
const daysIdx = args.indexOf('--days');
|
|
21
|
+
const days = daysIdx >= 0 ? parseInt(args[daysIdx + 1]) || 30 : 30;
|
|
22
|
+
|
|
23
|
+
const configDir = path.join(projectRoot, '.devassist');
|
|
24
|
+
if (!fs.existsSync(configDir)) {
|
|
25
|
+
console.log(` ${c.yellow('项目未初始化。')} 运行 devassist init 后再使用清理。\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cutoff = new Date();
|
|
30
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
31
|
+
|
|
32
|
+
let totalFiles = 0;
|
|
33
|
+
let totalSize = 0;
|
|
34
|
+
|
|
35
|
+
// 1. Clean reports
|
|
36
|
+
const reportDir = path.join(configDir, 'reports');
|
|
37
|
+
if (fs.existsSync(reportDir)) {
|
|
38
|
+
console.log(` ${c.bold('报告目录 (.devassist/reports/)')}`);
|
|
39
|
+
const files = fs.readdirSync(reportDir);
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (file === 'latest.html' || file === 'latest.json') continue; // always keep latest
|
|
42
|
+
const filePath = path.join(reportDir, file);
|
|
43
|
+
const stat = fs.statSync(filePath);
|
|
44
|
+
const shouldClean = cleanAll || stat.mtime < cutoff;
|
|
45
|
+
if (shouldClean) {
|
|
46
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
47
|
+
console.log(` ${c.gray('删除')} ${file} ${c.gray(`(${sizeKB} KB, ${stat.mtime.toISOString().slice(0, 10)})`)}`);
|
|
48
|
+
totalFiles++;
|
|
49
|
+
totalSize += stat.size;
|
|
50
|
+
if (!dryRun) fs.unlinkSync(filePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Clean backups
|
|
57
|
+
const backupDir = path.join(configDir, 'backups');
|
|
58
|
+
if (fs.existsSync(backupDir)) {
|
|
59
|
+
console.log(` ${c.bold('备份目录 (.devassist/backups/)')}`);
|
|
60
|
+
const entries = fs.readdirSync(backupDir, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
const dirPath = path.join(backupDir, entry.name);
|
|
64
|
+
const manifestPath = path.join(dirPath, 'manifest.json');
|
|
65
|
+
let dirTime = new Date(0);
|
|
66
|
+
if (fs.existsSync(manifestPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
69
|
+
dirTime = new Date(manifest.createdAt || 0);
|
|
70
|
+
} catch (_) { dirTime = fs.statSync(dirPath).mtime; }
|
|
71
|
+
} else {
|
|
72
|
+
dirTime = fs.statSync(dirPath).mtime;
|
|
73
|
+
}
|
|
74
|
+
const shouldClean = cleanAll || dirTime < cutoff;
|
|
75
|
+
if (shouldClean) {
|
|
76
|
+
// Calculate size
|
|
77
|
+
let dirSize = 0;
|
|
78
|
+
const innerFiles = fs.readdirSync(dirPath);
|
|
79
|
+
for (const f of innerFiles) {
|
|
80
|
+
dirSize += fs.statSync(path.join(dirPath, f)).size;
|
|
81
|
+
}
|
|
82
|
+
const sizeKB = (dirSize / 1024).toFixed(1);
|
|
83
|
+
console.log(` ${c.gray('删除')} ${entry.name}/ ${c.gray(`(${sizeKB} KB, ${dirTime.toISOString().slice(0, 10)})`)}`);
|
|
84
|
+
totalFiles++;
|
|
85
|
+
totalSize += dirSize;
|
|
86
|
+
if (!dryRun) {
|
|
87
|
+
for (const f of innerFiles) fs.unlinkSync(path.join(dirPath, f));
|
|
88
|
+
fs.rmdirSync(dirPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3. Clean history
|
|
97
|
+
const historyDir = path.join(configDir, 'history');
|
|
98
|
+
if (fs.existsSync(historyDir)) {
|
|
99
|
+
console.log(` ${c.bold('历史记录目录 (.devassist/history/)')}`);
|
|
100
|
+
const files = fs.readdirSync(historyDir);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
if (!file.endsWith('.json')) continue;
|
|
103
|
+
const filePath = path.join(historyDir, file);
|
|
104
|
+
const stat = fs.statSync(filePath);
|
|
105
|
+
const fileDate = new Date(file.replace('.json', ''));
|
|
106
|
+
const shouldClean = cleanAll || fileDate < cutoff;
|
|
107
|
+
if (shouldClean) {
|
|
108
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
109
|
+
console.log(` ${c.gray('删除')} ${file} ${c.gray(`(${sizeKB} KB)`)}`);
|
|
110
|
+
totalFiles++;
|
|
111
|
+
totalSize += stat.size;
|
|
112
|
+
if (!dryRun) fs.unlinkSync(filePath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Summary
|
|
119
|
+
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
|
|
120
|
+
if (totalFiles === 0) {
|
|
121
|
+
console.log(` ${c.green('无需清理。')} 没有超过 ${days} 天的旧文件。\n`);
|
|
122
|
+
} else {
|
|
123
|
+
const action = dryRun ? '将删除' : '已删除';
|
|
124
|
+
console.log(` ${c.green(action + ' ' + totalFiles + ' 个文件')},释放 ${sizeMB} MB 空间。`);
|
|
125
|
+
if (dryRun) {
|
|
126
|
+
console.log(` ${c.gray('(--dry-run 模式,未实际删除。移除 --dry-run 执行清理。)')}`);
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
};
|