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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist diff - Check only changed files (CI mode)
|
|
3
|
+
*
|
|
4
|
+
* Uses git diff to identify changed files and runs checks only on those.
|
|
5
|
+
* Ideal for CI/CD pipelines and pre-commit hooks.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* devassist diff # Check unstaged changes
|
|
9
|
+
* devassist diff --staged # Check only staged files
|
|
10
|
+
* devassist diff --base main # Compare against branch
|
|
11
|
+
* devassist diff --json # JSON output for CI integration
|
|
12
|
+
* devassist diff --fail-on warn # Exit non-zero on warnings (default: block only)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
// Core
|
|
20
|
+
const { engine } = require('../../core/rule-engine');
|
|
21
|
+
|
|
22
|
+
// Modules
|
|
23
|
+
const { ConventionStore } = require('../../modules/dev-time/convention-store');
|
|
24
|
+
const { codeQualityRules } = require('../../modules/dev-time/code-quality-guard');
|
|
25
|
+
const { ArchRiskAssessor } = require('../../modules/dev-time/arch-risk-assessor');
|
|
26
|
+
const { ImpactAnalyzer } = require('../../modules/dev-time/impact-analyzer');
|
|
27
|
+
|
|
28
|
+
// Shared utilities
|
|
29
|
+
const { loadIgnorePatterns, filterByIgnorePatterns } = require('../shared/file-collector');
|
|
30
|
+
const { loadConfig, applyConfigToFindings } = require('../shared/config-loader');
|
|
31
|
+
const { filterIgnored } = require('../shared/inline-ignore');
|
|
32
|
+
|
|
33
|
+
const CODE_EXTENSIONS = /\.(js|ts|jsx|tsx|py|java|html)$/;
|
|
34
|
+
|
|
35
|
+
module.exports = async function diff(projectRoot, args, ui) {
|
|
36
|
+
const { printHeader, printFinding, printStats, c } = ui;
|
|
37
|
+
const staged = args.includes('--staged');
|
|
38
|
+
const jsonOutput = args.includes('--json');
|
|
39
|
+
const baseBranch = args.includes('--base') ? args[args.indexOf('--base') + 1] : null;
|
|
40
|
+
const failOn = args.includes('--fail-on') ? args[args.indexOf('--fail-on') + 1] : 'block';
|
|
41
|
+
|
|
42
|
+
if (!jsonOutput) printHeader('差异检查');
|
|
43
|
+
|
|
44
|
+
// Get changed files from git
|
|
45
|
+
let changedFiles = [];
|
|
46
|
+
try {
|
|
47
|
+
let gitCmd;
|
|
48
|
+
if (staged) {
|
|
49
|
+
gitCmd = 'git diff --cached --name-only --diff-filter=ACMR';
|
|
50
|
+
} else if (baseBranch) {
|
|
51
|
+
gitCmd = `git diff ${baseBranch}...HEAD --name-only --diff-filter=ACMR`;
|
|
52
|
+
} else {
|
|
53
|
+
gitCmd = 'git diff --name-only --diff-filter=ACMR';
|
|
54
|
+
}
|
|
55
|
+
const output = execSync(gitCmd, { cwd: projectRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
56
|
+
changedFiles = output ? output.split('\n').filter(f => f) : [];
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (jsonOutput) {
|
|
59
|
+
console.log(JSON.stringify({ error: '非 Git 仓库或 Git 不可用', findings: [] }));
|
|
60
|
+
} else {
|
|
61
|
+
console.log(` ${c.yellow('非 Git 仓库或未检测到变更。')}\n`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Filter to code files that exist
|
|
67
|
+
changedFiles = changedFiles.filter(f => {
|
|
68
|
+
if (!CODE_EXTENSIONS.test(f)) return false;
|
|
69
|
+
const fullPath = path.join(projectRoot, f);
|
|
70
|
+
return fs.existsSync(fullPath);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Also skip files in ignored directories
|
|
74
|
+
changedFiles = filterByIgnorePatterns(changedFiles, projectRoot);
|
|
75
|
+
|
|
76
|
+
if (changedFiles.length === 0) {
|
|
77
|
+
if (jsonOutput) {
|
|
78
|
+
console.log(JSON.stringify({ findings: [], stats: { filesChecked: 0 } }));
|
|
79
|
+
} else {
|
|
80
|
+
console.log(` ${c.green('无代码文件变更。')}\n`);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!jsonOutput) {
|
|
86
|
+
console.log(` ${c.gray('正在检查')} ${changedFiles.length} ${c.gray('个变更文件:')}`);
|
|
87
|
+
for (const f of changedFiles) {
|
|
88
|
+
console.log(` ${c.gray(f)}`);
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Read file contents
|
|
94
|
+
const fileContents = changedFiles.map(f => {
|
|
95
|
+
const fullPath = path.join(projectRoot, f);
|
|
96
|
+
let content = '';
|
|
97
|
+
try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (_) {}
|
|
98
|
+
return { path: f, content, fullPath };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Skip HTML without script tags
|
|
102
|
+
const validFiles = fileContents.filter(f => {
|
|
103
|
+
if (/\.html?$/.test(f.path) && !/<script[\s>]/i.test(f.content)) return false;
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const ctx = {
|
|
108
|
+
files: validFiles,
|
|
109
|
+
projectRoot,
|
|
110
|
+
config: loadConfig(projectRoot),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Register and run rules
|
|
114
|
+
engine.registerBatch(codeQualityRules);
|
|
115
|
+
|
|
116
|
+
const conventionStore = new ConventionStore(projectRoot);
|
|
117
|
+
engine.register({
|
|
118
|
+
id: 'convention-check',
|
|
119
|
+
severity: 'warn',
|
|
120
|
+
category: 'convention',
|
|
121
|
+
check(ctx) {
|
|
122
|
+
const allFindings = [];
|
|
123
|
+
for (const file of ctx.files) {
|
|
124
|
+
const findings = conventionStore.check(file.path, file.content);
|
|
125
|
+
allFindings.push(...findings);
|
|
126
|
+
}
|
|
127
|
+
return allFindings.length > 0 ? allFindings : null;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { findings: ruleFindings } = engine.run(ctx);
|
|
132
|
+
|
|
133
|
+
// Arch assessment
|
|
134
|
+
const archAssessor = new ArchRiskAssessor(projectRoot);
|
|
135
|
+
const archFindings = [];
|
|
136
|
+
for (const file of validFiles) {
|
|
137
|
+
const findings = archAssessor.analyzeFile(file.path, file.content);
|
|
138
|
+
archFindings.push(...findings);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Impact analysis on changed files
|
|
142
|
+
let impactFindings = [];
|
|
143
|
+
try {
|
|
144
|
+
const impactAnalyzer = new ImpactAnalyzer(projectRoot);
|
|
145
|
+
impactAnalyzer.buildGraph();
|
|
146
|
+
for (const file of validFiles) {
|
|
147
|
+
const result = impactAnalyzer.analyzeChange(file.fullPath, file.content);
|
|
148
|
+
impactFindings.push(...result.findings);
|
|
149
|
+
}
|
|
150
|
+
} catch (_) {}
|
|
151
|
+
|
|
152
|
+
let allFindings = [...ruleFindings, ...archFindings, ...impactFindings];
|
|
153
|
+
|
|
154
|
+
// Apply config overrides and inline ignores
|
|
155
|
+
allFindings = applyConfigToFindings(ctx.config, allFindings);
|
|
156
|
+
allFindings = filterIgnored(allFindings, validFiles);
|
|
157
|
+
|
|
158
|
+
// Output
|
|
159
|
+
if (jsonOutput) {
|
|
160
|
+
console.log(JSON.stringify({
|
|
161
|
+
findings: allFindings,
|
|
162
|
+
stats: {
|
|
163
|
+
filesChecked: validFiles.length,
|
|
164
|
+
blocks: allFindings.filter(f => f.severity === 'block').length,
|
|
165
|
+
warnings: allFindings.filter(f => f.severity === 'warn').length,
|
|
166
|
+
infos: allFindings.filter(f => f.severity === 'info').length,
|
|
167
|
+
},
|
|
168
|
+
}, null, 2));
|
|
169
|
+
} else {
|
|
170
|
+
if (allFindings.length === 0) {
|
|
171
|
+
console.log(` ${c.green('所有变更文件检查通过!')}\n`);
|
|
172
|
+
} else {
|
|
173
|
+
for (const f of allFindings) {
|
|
174
|
+
printFinding(f);
|
|
175
|
+
}
|
|
176
|
+
printStats({
|
|
177
|
+
blocks: allFindings.filter(f => f.severity === 'block').length,
|
|
178
|
+
warnings: allFindings.filter(f => f.severity === 'warn').length,
|
|
179
|
+
infos: allFindings.filter(f => f.severity === 'info').length,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Exit code for CI
|
|
185
|
+
const hasBlocks = allFindings.some(f => f.severity === 'block');
|
|
186
|
+
const hasWarns = allFindings.some(f => f.severity === 'warn');
|
|
187
|
+
if (hasBlocks || (failOn === 'warn' && hasWarns)) {
|
|
188
|
+
if (!jsonOutput) {
|
|
189
|
+
console.log(`\n ${c.red('检查未通过。')} ${hasBlocks ? '发现阻断级别问题。' : '警告被视为失败(--fail-on warn)。'}\n`);
|
|
190
|
+
}
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist doctor - Environment diagnostics
|
|
3
|
+
*
|
|
4
|
+
* Checks:
|
|
5
|
+
* 1. Node.js version
|
|
6
|
+
* 2. Git availability
|
|
7
|
+
* 3. .devassist/ config completeness
|
|
8
|
+
* 4. AI provider configuration
|
|
9
|
+
* 5. .devassistignore presence
|
|
10
|
+
* 6. Git pre-commit hook
|
|
11
|
+
* 7. Report/backup/history directories
|
|
12
|
+
* 8. config.json validity
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
module.exports = function doctor(projectRoot, args, ui) {
|
|
20
|
+
const { printHeader, c } = ui;
|
|
21
|
+
printHeader('环境诊断');
|
|
22
|
+
|
|
23
|
+
const configDir = path.join(projectRoot, '.devassist');
|
|
24
|
+
const checks = [];
|
|
25
|
+
|
|
26
|
+
// 1. Node.js version
|
|
27
|
+
try {
|
|
28
|
+
const nodeVersion = process.version;
|
|
29
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
30
|
+
const ok = major >= 14;
|
|
31
|
+
checks.push({
|
|
32
|
+
name: 'Node.js 版本',
|
|
33
|
+
status: ok ? 'pass' : 'fail',
|
|
34
|
+
detail: `${nodeVersion} ${ok ? '' : '(需要 v14+)'}`,
|
|
35
|
+
});
|
|
36
|
+
} catch (_) {
|
|
37
|
+
checks.push({ name: 'Node.js 版本', status: 'fail', detail: '无法检测' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Git availability
|
|
41
|
+
try {
|
|
42
|
+
const gitVersion = execSync('git --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
43
|
+
checks.push({ name: 'Git 可用性', status: 'pass', detail: gitVersion });
|
|
44
|
+
} catch (_) {
|
|
45
|
+
checks.push({ name: 'Git 可用性', status: 'warn', detail: 'Git 未安装或不在 PATH 中' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. .devassist/ directory
|
|
49
|
+
if (fs.existsSync(configDir)) {
|
|
50
|
+
checks.push({ name: '.devassist/ 目录', status: 'pass', detail: '存在' });
|
|
51
|
+
} else {
|
|
52
|
+
checks.push({ name: '.devassist/ 目录', status: 'fail', detail: '不存在——运行 devassist init' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 4. Config files
|
|
56
|
+
const requiredFiles = [
|
|
57
|
+
'conventions.json',
|
|
58
|
+
'schema-registry.json',
|
|
59
|
+
'tech-debt.json',
|
|
60
|
+
'ai-config.json',
|
|
61
|
+
'config.json',
|
|
62
|
+
];
|
|
63
|
+
for (const file of requiredFiles) {
|
|
64
|
+
const filePath = path.join(configDir, file);
|
|
65
|
+
if (fs.existsSync(filePath)) {
|
|
66
|
+
// Validate JSON
|
|
67
|
+
try {
|
|
68
|
+
JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
69
|
+
checks.push({ name: `配置文件 ${file}`, status: 'pass', detail: '存在且格式正确' });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
checks.push({ name: `配置文件 ${file}`, status: 'fail', detail: `JSON 格式错误:${e.message}` });
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
checks.push({ name: `配置文件 ${file}`, status: 'warn', detail: '不存在' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5. AI provider configuration (multi-engine)
|
|
79
|
+
const aiConfigPath = path.join(configDir, 'ai-config.json');
|
|
80
|
+
if (fs.existsSync(aiConfigPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const aiConfig = JSON.parse(fs.readFileSync(aiConfigPath, 'utf-8'));
|
|
83
|
+
const envMap = { qwen: 'DASHSCOPE_API_KEY', openai: 'OPENAI_API_KEY', claude: 'ANTHROPIC_API_KEY', hunyuan: 'HUNYUAN_API_KEY' };
|
|
84
|
+
|
|
85
|
+
// Collect all engines from config
|
|
86
|
+
const allEngines = [];
|
|
87
|
+
if (aiConfig.provider) {
|
|
88
|
+
allEngines.push({ provider: aiConfig.provider, apiKey: aiConfig.apiKey, enabled: aiConfig.enabled !== false, isPrimary: true });
|
|
89
|
+
}
|
|
90
|
+
if (aiConfig.secondary && aiConfig.secondary.provider) {
|
|
91
|
+
allEngines.push({ provider: aiConfig.secondary.provider, apiKey: aiConfig.secondary.apiKey, enabled: aiConfig.secondary.enabled !== false, isPrimary: false });
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(aiConfig.engines)) {
|
|
94
|
+
for (const eng of aiConfig.engines) {
|
|
95
|
+
if (eng && eng.provider) {
|
|
96
|
+
allEngines.push({ provider: eng.provider, apiKey: eng.apiKey, enabled: eng.enabled !== false, isPrimary: false });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Deduplicate by provider
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const uniqueEngines = allEngines.filter(e => {
|
|
104
|
+
if (seen.has(e.provider)) return false;
|
|
105
|
+
seen.add(e.provider);
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (uniqueEngines.length === 0) {
|
|
110
|
+
checks.push({ name: 'AI 引擎', status: 'warn', detail: '未启用(可选功能)' });
|
|
111
|
+
} else {
|
|
112
|
+
for (let i = 0; i < uniqueEngines.length; i++) {
|
|
113
|
+
const eng = uniqueEngines[i];
|
|
114
|
+
const envVar = envMap[eng.provider];
|
|
115
|
+
const hasKey = eng.apiKey || (envVar && process.env[envVar]);
|
|
116
|
+
const label = `AI 引擎 [${i}]${eng.isPrimary ? ' 主' : ''}`;
|
|
117
|
+
checks.push({
|
|
118
|
+
name: label,
|
|
119
|
+
status: hasKey ? 'pass' : 'warn',
|
|
120
|
+
detail: `${eng.provider} ${hasKey ? '(API Key 已配置)' : '(API Key 未设置)'}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (uniqueEngines.filter(e => e.apiKey || process.env[envMap[e.provider]]).length >= 2) {
|
|
124
|
+
checks.push({ name: 'AI 多引擎', status: 'pass', detail: `${uniqueEngines.length} 个引擎就绪,可运行 --compare` });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (_) {
|
|
128
|
+
checks.push({ name: 'AI 引擎', status: 'warn', detail: '配置文件无法读取' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 6. .devassistignore
|
|
133
|
+
const ignorePath = path.join(projectRoot, '.devassistignore');
|
|
134
|
+
checks.push({
|
|
135
|
+
name: '.devassistignore',
|
|
136
|
+
status: fs.existsSync(ignorePath) ? 'pass' : 'warn',
|
|
137
|
+
detail: fs.existsSync(ignorePath) ? '存在' : '不存在(建议创建)',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// 7. Git pre-commit hook
|
|
141
|
+
const hookPath = path.join(projectRoot, '.git', 'hooks', 'pre-commit');
|
|
142
|
+
if (fs.existsSync(hookPath)) {
|
|
143
|
+
const hookContent = fs.readFileSync(hookPath, 'utf-8');
|
|
144
|
+
if (hookContent.includes('devassist')) {
|
|
145
|
+
checks.push({ name: 'Git pre-commit 钩子', status: 'pass', detail: '已安装' });
|
|
146
|
+
} else {
|
|
147
|
+
checks.push({ name: 'Git pre-commit 钩子', status: 'warn', detail: '存在但不包含 DevAssist' });
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
checks.push({ name: 'Git pre-commit 钩子', status: 'warn', detail: '未安装(可选)' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 8. Directories
|
|
154
|
+
const dirs = ['reports', 'backups', 'history'];
|
|
155
|
+
for (const dir of dirs) {
|
|
156
|
+
const dirPath = path.join(configDir, dir);
|
|
157
|
+
checks.push({
|
|
158
|
+
name: `目录 .devassist/${dir}/`,
|
|
159
|
+
status: fs.existsSync(dirPath) ? 'pass' : 'warn',
|
|
160
|
+
detail: fs.existsSync(dirPath) ? '存在' : '不存在',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Print results
|
|
165
|
+
let passCount = 0, warnCount = 0, failCount = 0;
|
|
166
|
+
for (const check of checks) {
|
|
167
|
+
const icon = check.status === 'pass' ? c.green('[通过]') :
|
|
168
|
+
check.status === 'warn' ? c.yellow('[警告]') :
|
|
169
|
+
c.red('[失败]');
|
|
170
|
+
console.log(` ${icon} ${c.bold(check.name)}:${c.gray(check.detail)}`);
|
|
171
|
+
if (check.status === 'pass') passCount++;
|
|
172
|
+
else if (check.status === 'warn') warnCount++;
|
|
173
|
+
else failCount++;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(c.gray(' ────────────────────────────────────────'));
|
|
177
|
+
console.log(` ${c.green(`${passCount} 通过`)} ${c.yellow(`${warnCount} 警告`)} ${c.red(`${failCount} 失败`)}`);
|
|
178
|
+
|
|
179
|
+
if (failCount > 0) {
|
|
180
|
+
console.log(`\n ${c.red('存在失败项。')} 请按上述提示修复后再使用。\n`);
|
|
181
|
+
} else if (warnCount > 0) {
|
|
182
|
+
console.log(`\n ${c.yellow('存在警告项。')} 这些不影响基本功能,但建议修复以获得完整体验。\n`);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(`\n ${c.green('全部检查通过!')} 环境配置完整。\n`);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist fix - Auto-fix common code issues
|
|
3
|
+
*
|
|
4
|
+
* Supported fixes:
|
|
5
|
+
* --fetch-catch Add .catch() to unhandled fetch() calls
|
|
6
|
+
* --remove-console Remove console.log statements (replace with comment)
|
|
7
|
+
* --add-await Add await to async function calls missing it
|
|
8
|
+
* --all Apply all safe fixes
|
|
9
|
+
* --dry-run Show what would be fixed without modifying files
|
|
10
|
+
*
|
|
11
|
+
* Safety:
|
|
12
|
+
* - Always creates a .devassist/backups/fix-<timestamp>/ backup before modifying
|
|
13
|
+
* - Only applies fixes that are unlikely to change behavior
|
|
14
|
+
* - Prints a summary of all changes
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const { collectFileContents } = require('../shared/file-collector');
|
|
21
|
+
|
|
22
|
+
module.exports = function fix(projectRoot, args, ui) {
|
|
23
|
+
const { printHeader, c } = ui;
|
|
24
|
+
printHeader('自动修复');
|
|
25
|
+
|
|
26
|
+
const dryRun = args.includes('--dry-run');
|
|
27
|
+
const fixAll = args.includes('--all');
|
|
28
|
+
const fixFetchCatch = fixAll || args.includes('--fetch-catch');
|
|
29
|
+
const fixRemoveConsole = fixAll || args.includes('--remove-console');
|
|
30
|
+
const fixAddAwait = fixAll || args.includes('--add-await');
|
|
31
|
+
|
|
32
|
+
if (!fixFetchCatch && !fixRemoveConsole && !fixAddAwait) {
|
|
33
|
+
console.log(` ${c.yellow('未指定修复项。')}\n`);
|
|
34
|
+
console.log(` ${c.bold('可用修复项:')}`);
|
|
35
|
+
console.log(` ${c.green('--fetch-catch')} 为 fetch() 调用添加 .catch() 错误处理`);
|
|
36
|
+
console.log(` ${c.green('--remove-console')} 移除 console.log 调试语句`);
|
|
37
|
+
console.log(` ${c.green('--add-await')} 为 async 函数调用添加 await`);
|
|
38
|
+
console.log(` ${c.green('--all')} 应用所有安全修复`);
|
|
39
|
+
console.log(` ${c.gray('--dry-run')} 仅预览,不实际修改文件\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fileContents = collectFileContents(projectRoot);
|
|
44
|
+
const changes = []; // { file, line, before, after, type }
|
|
45
|
+
|
|
46
|
+
for (const file of fileContents) {
|
|
47
|
+
if (!file.content || !/\.(js|ts|jsx|tsx)$/.test(file.path)) continue;
|
|
48
|
+
// Skip this tool's own source
|
|
49
|
+
if (/devassist|node_modules/.test(file.path)) continue;
|
|
50
|
+
|
|
51
|
+
let content = file.content;
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
|
|
54
|
+
// Fix 1: fetch() without .catch()
|
|
55
|
+
if (fixFetchCatch) {
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const line = lines[i];
|
|
58
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue; // skip comments
|
|
59
|
+
if (!/\bfetch\s*\(/.test(line)) continue;
|
|
60
|
+
|
|
61
|
+
// Check if .catch is already present nearby
|
|
62
|
+
const context = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
63
|
+
if (/\.catch\s*\(/.test(context)) continue;
|
|
64
|
+
|
|
65
|
+
// Check if it's an await fetch in try/catch
|
|
66
|
+
const hasAwait = /\bawait\b/.test(line);
|
|
67
|
+
const hasTryCatch = /try\s*\{/.test(lines.slice(Math.max(0, i - 5), i + 1).join('\n'));
|
|
68
|
+
if (hasAwait && hasTryCatch) continue;
|
|
69
|
+
|
|
70
|
+
// Add .catch() — simple case: single-line fetch call
|
|
71
|
+
const trimmed = line.trimEnd();
|
|
72
|
+
if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
|
|
73
|
+
const indent = line.match(/^\s*/)[0];
|
|
74
|
+
const fixedLine = trimmed.replace(/\)\s*$/, ').catch(err => console.error(\'fetch error:\', err))');
|
|
75
|
+
changes.push({
|
|
76
|
+
file: file.path, line: i + 1, type: 'fetch-catch',
|
|
77
|
+
before: line, after: fixedLine,
|
|
78
|
+
});
|
|
79
|
+
if (!dryRun) lines[i] = fixedLine;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fix 2: Remove console.log
|
|
85
|
+
if (fixRemoveConsole) {
|
|
86
|
+
// Skip CLI command files — they use console.log for intentional terminal output
|
|
87
|
+
const isCliFile = /cli[\\/]/.test(file.path);
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i];
|
|
90
|
+
if (isCliFile) break; // skip entire file
|
|
91
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue;
|
|
92
|
+
if (!/console\.log\s*\(/.test(line)) continue;
|
|
93
|
+
// Skip logger files
|
|
94
|
+
if (/logger/i.test(file.path)) continue;
|
|
95
|
+
|
|
96
|
+
const indent = line.match(/^\s*/)[0];
|
|
97
|
+
const fixedLine = `${indent}// devassist-fix: removed console.log`;
|
|
98
|
+
changes.push({
|
|
99
|
+
file: file.path, line: i + 1, type: 'remove-console',
|
|
100
|
+
before: line.trim(), after: fixedLine.trim(),
|
|
101
|
+
});
|
|
102
|
+
if (!dryRun) lines[i] = fixedLine;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fix 3: Add await to async calls
|
|
107
|
+
if (fixAddAwait) {
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue;
|
|
111
|
+
if (/\bawait\b/.test(line)) continue;
|
|
112
|
+
|
|
113
|
+
// Only match known async patterns (not generic function names)
|
|
114
|
+
const asyncMatch = line.match(/(?<![\w.])(fetch|axios\.(get|post|put|delete|patch)|\.save\(|\.findOne\(|\.findById\(|\.create\()\s*/);
|
|
115
|
+
if (!asyncMatch) continue;
|
|
116
|
+
|
|
117
|
+
// Skip if it's part of assignment or condition
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (/^\s*(const|let|var|if|while|return|throw)/.test(trimmed)) continue;
|
|
120
|
+
// Skip function declarations (function getProvider(...) is not a call)
|
|
121
|
+
if (/^\s*(async\s+)?function\s+/.test(line)) continue;
|
|
122
|
+
if (/^\s*(export\s+)?(async\s+)?function\s+/.test(line)) continue;
|
|
123
|
+
if (/\.then\s*\(|\.catch\s*\(|\.finally\s*\(/.test(line)) continue;
|
|
124
|
+
|
|
125
|
+
// Add await before the function call
|
|
126
|
+
const fixedLine = line.replace(
|
|
127
|
+
new RegExp('(?<![\\w.])(' + asyncMatch[1] + '\\w+)\\s*\\('),
|
|
128
|
+
'await $1('
|
|
129
|
+
);
|
|
130
|
+
if (fixedLine !== line) {
|
|
131
|
+
changes.push({
|
|
132
|
+
file: file.path, line: i + 1, type: 'add-await',
|
|
133
|
+
before: line.trim(), after: fixedLine.trim(),
|
|
134
|
+
});
|
|
135
|
+
if (!dryRun) lines[i] = fixedLine;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Write back if changes were made
|
|
141
|
+
if (!dryRun && changes.some(ch => ch.file === file.path)) {
|
|
142
|
+
const newContent = lines.join('\n');
|
|
143
|
+
if (newContent !== content) {
|
|
144
|
+
// Create backup of original file
|
|
145
|
+
const backupDir = path.join(projectRoot, '.devassist', 'backups', `fix-${Date.now()}`);
|
|
146
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
147
|
+
const backupPath = path.join(backupDir, file.path.replace(/[\\/]/g, '_'));
|
|
148
|
+
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
149
|
+
fs.writeFileSync(file.fullPath, newContent, 'utf-8');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Print summary
|
|
155
|
+
if (changes.length === 0) {
|
|
156
|
+
console.log(` ${c.green('无需修复。')} 没有发现可自动修复的问题。\n`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Group by type
|
|
161
|
+
const byType = {};
|
|
162
|
+
for (const ch of changes) {
|
|
163
|
+
if (!byType[ch.type]) byType[ch.type] = [];
|
|
164
|
+
byType[ch.type].push(ch);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const typeLabels = {
|
|
168
|
+
'fetch-catch': 'fetch 添加 .catch()',
|
|
169
|
+
'remove-console': '移除 console.log',
|
|
170
|
+
'add-await': '添加 await',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
console.log(` ${c.bold('修复摘要:')}\n`);
|
|
174
|
+
for (const [type, items] of Object.entries(byType)) {
|
|
175
|
+
console.log(` ${c.green(typeLabels[type] || type)}:${items.length} 处`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Show details
|
|
179
|
+
console.log(`\n ${c.bold('详情:')}\n`);
|
|
180
|
+
for (const ch of changes.slice(0, 20)) {
|
|
181
|
+
console.log(` ${c.gray(ch.file + ':' + ch.line)} [${typeLabels[ch.type] || ch.type}]`);
|
|
182
|
+
console.log(` ${c.red('-')} ${ch.before.substring(0, 80)}`);
|
|
183
|
+
console.log(` ${c.green('+')} ${ch.after.substring(0, 80)}`);
|
|
184
|
+
}
|
|
185
|
+
if (changes.length > 20) {
|
|
186
|
+
console.log(` ${c.gray(`...还有 ${changes.length - 20} 处修改`)}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (dryRun) {
|
|
190
|
+
console.log(`\n ${c.gray('(--dry-run 模式,未实际修改文件。移除 --dry-run 执行修复。)')}`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`\n ${c.green('已修复 ' + changes.length + ' 处问题。')} 原文件已备份到 .devassist/backups/`);
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
};
|