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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/bin/devassist.js +220 -0
  4. package/package.json +44 -0
  5. package/src/ai/adapter.js +464 -0
  6. package/src/ai/providers/claude.js +80 -0
  7. package/src/ai/providers/hunyuan.js +87 -0
  8. package/src/ai/providers/openai.js +74 -0
  9. package/src/ai/providers/qwen.js +81 -0
  10. package/src/cli/commands/ai.js +944 -0
  11. package/src/cli/commands/ask.js +79 -0
  12. package/src/cli/commands/backup.js +30 -0
  13. package/src/cli/commands/check.js +327 -0
  14. package/src/cli/commands/clean.js +130 -0
  15. package/src/cli/commands/comparison-report.js +326 -0
  16. package/src/cli/commands/convention.js +91 -0
  17. package/src/cli/commands/debt.js +49 -0
  18. package/src/cli/commands/deploy.js +88 -0
  19. package/src/cli/commands/diff.js +193 -0
  20. package/src/cli/commands/doctor.js +186 -0
  21. package/src/cli/commands/fix.js +195 -0
  22. package/src/cli/commands/init.js +431 -0
  23. package/src/cli/commands/inject.js +254 -0
  24. package/src/cli/commands/report.js +310 -0
  25. package/src/cli/commands/restore.js +78 -0
  26. package/src/cli/commands/schema.js +93 -0
  27. package/src/cli/commands/watch.js +212 -0
  28. package/src/cli/shared/code-context.js +51 -0
  29. package/src/cli/shared/config-loader.js +89 -0
  30. package/src/cli/shared/file-collector.js +116 -0
  31. package/src/cli/shared/inline-ignore.js +142 -0
  32. package/src/cli/shared/watch-list.js +281 -0
  33. package/src/core/event-bus.js +83 -0
  34. package/src/core/fsm.js +103 -0
  35. package/src/core/logger.js +54 -0
  36. package/src/core/rule-engine.js +117 -0
  37. package/src/index.js +64 -0
  38. package/src/modules/dev-time/arch-risk-assessor.js +250 -0
  39. package/src/modules/dev-time/code-quality-guard.js +1340 -0
  40. package/src/modules/dev-time/convention-store.js +201 -0
  41. package/src/modules/dev-time/impact-analyzer.js +292 -0
  42. package/src/modules/dev-time/pre-deploy-guard.js +284 -0
  43. package/src/modules/dev-time/schema-registry.js +284 -0
  44. package/src/modules/dev-time/tech-debt-tracker.js +225 -0
  45. package/src/modules/dev-time/version-manager.js +280 -0
  46. package/src/modules/runtime/channel-agent.js +404 -0
  47. package/src/modules/runtime/channel-middleware.js +316 -0
  48. package/src/modules/runtime/index.js +64 -0
  49. package/src/modules/runtime/infrastructure-guard.js +582 -0
  50. package/src/modules/runtime/notification-center.js +443 -0
  51. package/src/modules/runtime/schema-gatekeeper.js +664 -0
  52. package/src/modules/runtime/tool-registry.js +329 -0
  53. package/templates/ci/github-actions.yml +60 -0
  54. package/templates/ci/gitlab-ci.yml +30 -0
  55. package/templates/ci/pre-commit-hook.sh +18 -0
  56. package/templates/git-hooks/pre-commit +61 -0
  57. package/tests/run-all.js +434 -0
  58. package/tests/test-layer2.js +461 -0
  59. 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
+ };