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,310 @@
1
+ /**
2
+ * devassist report - Generate HTML check report
3
+ *
4
+ * Runs full check and generates a styled HTML report with:
5
+ * - Summary statistics
6
+ * - Findings grouped by severity
7
+ * - File distribution
8
+ * - Rule trigger statistics
9
+ * - Fix suggestions
10
+ *
11
+ * Usage:
12
+ * devassist report # Generate report
13
+ * devassist report --ai # Include AI analysis
14
+ * devassist report --open # Open in browser after generating
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { execSync } = require('child_process');
20
+
21
+ // Core
22
+ const { engine } = require('../../core/rule-engine');
23
+
24
+ // Modules
25
+ const { ConventionStore } = require('../../modules/dev-time/convention-store');
26
+ const { codeQualityRules } = require('../../modules/dev-time/code-quality-guard');
27
+ const { ImpactAnalyzer } = require('../../modules/dev-time/impact-analyzer');
28
+ const { ArchRiskAssessor } = require('../../modules/dev-time/arch-risk-assessor');
29
+ const { TechDebtTracker } = require('../../modules/dev-time/tech-debt-tracker');
30
+
31
+ // Shared utilities
32
+ const { collectFileContents } = require('../shared/file-collector');
33
+ const { loadConfig, applyConfigToFindings } = require('../shared/config-loader');
34
+ const { filterIgnored } = require('../shared/inline-ignore');
35
+
36
+ module.exports = async function report(projectRoot, args, ui) {
37
+ const { printHeader, c } = ui;
38
+ const useAI = args.includes('--ai');
39
+ const openBrowser = args.includes('--open');
40
+
41
+ printHeader('生成报告');
42
+
43
+ // Collect files using shared utility
44
+ const fileContents = collectFileContents(projectRoot);
45
+ if (fileContents.length === 0) {
46
+ console.log(` ${c.yellow('未找到源文件。')}\n`);
47
+ return;
48
+ }
49
+
50
+ const config = loadConfig(projectRoot);
51
+ const ctx = {
52
+ files: fileContents,
53
+ projectRoot,
54
+ config,
55
+ };
56
+
57
+ // Run all checks
58
+ console.log(` ${c.gray('正在执行检查...')}`);
59
+ engine.registerBatch(codeQualityRules);
60
+
61
+ const conventionStore = new ConventionStore(projectRoot);
62
+ engine.register({
63
+ id: 'convention-check',
64
+ severity: 'warn',
65
+ category: 'convention',
66
+ check(ctx) {
67
+ const allFindings = [];
68
+ for (const file of ctx.files) {
69
+ const findings = conventionStore.check(file.path, file.content);
70
+ allFindings.push(...findings);
71
+ }
72
+ return allFindings.length > 0 ? allFindings : null;
73
+ }
74
+ });
75
+
76
+ const { findings: ruleFindings } = engine.run(ctx);
77
+
78
+ // Arch assessment
79
+ const archAssessor = new ArchRiskAssessor(projectRoot);
80
+ const archFindings = [];
81
+ for (const file of fileContents) {
82
+ const findings = archAssessor.analyzeFile(file.path, file.content);
83
+ archFindings.push(...findings);
84
+ }
85
+
86
+ // Tech debt scan
87
+ const debtTracker = new TechDebtTracker(projectRoot);
88
+ const debtBoard = debtTracker.scan();
89
+
90
+ let allFindings = [...ruleFindings, ...archFindings];
91
+
92
+ // Apply config-based rule overrides and inline ignores
93
+ allFindings = applyConfigToFindings(config, allFindings);
94
+ allFindings = filterIgnored(allFindings, fileContents);
95
+
96
+ // Generate HTML report
97
+ const html = generateHTML({
98
+ projectRoot: path.basename(projectRoot),
99
+ timestamp: new Date().toISOString(),
100
+ findings: allFindings,
101
+ debtBoard,
102
+ totalLines: fileContents.reduce((sum, f) => sum + f.content.split('\n').length, 0),
103
+ fileCount: fileContents.length,
104
+ });
105
+
106
+ // Save report
107
+ const reportDir = path.join(projectRoot, '.devassist', 'reports');
108
+ if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
109
+ const now = new Date();
110
+ const dateStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}`;
111
+ const reportPath = path.join(reportDir, `report-${dateStr}.html`);
112
+ fs.writeFileSync(reportPath, html, 'utf-8');
113
+
114
+ // Also save as latest.html for easy access
115
+ fs.writeFileSync(path.join(reportDir, 'latest.html'), html, 'utf-8');
116
+
117
+ console.log(`\n ${c.green('报告已生成:')} ${path.relative(projectRoot, reportPath)}`);
118
+ console.log(` ${c.gray('同时保存为:')} .devassist/reports/latest.html\n`);
119
+
120
+ // Print summary
121
+ const blocks = allFindings.filter(f => f.severity === 'block').length;
122
+ const warns = allFindings.filter(f => f.severity === 'warn').length;
123
+ const infos = allFindings.filter(f => f.severity === 'info').length;
124
+ console.log(` ${c.bold('汇总:')}`);
125
+ console.log(` 扫描文件数:${fileContents.length}`);
126
+ console.log(` 总行数:${fileContents.reduce((sum, f) => sum + f.content.split('\n').length, 0)}`);
127
+ console.log(` ${c.red(`阻断:${blocks}`)} ${c.yellow(`警告:${warns}`)} ${c.gray(`提示:${infos}`)}`);
128
+ if (debtBoard.items && debtBoard.items.length > 0) {
129
+ console.log(` 技术债务项:${debtBoard.items.length}`);
130
+ }
131
+ console.log('');
132
+
133
+ // Open in browser if requested
134
+ if (openBrowser) {
135
+ try {
136
+ const { exec } = require('child_process');
137
+ if (process.platform === 'win32') {
138
+ exec(`start "" "${reportPath}"`);
139
+ } else if (process.platform === 'darwin') {
140
+ exec(`open "${reportPath}"`);
141
+ } else {
142
+ exec(`xdg-open "${reportPath}"`);
143
+ }
144
+ console.log(` ${c.gray('正在浏览器中打开...')}`);
145
+ } catch (_) {}
146
+ }
147
+ };
148
+
149
+ function generateHTML(data) {
150
+ const { projectRoot, timestamp, findings, debtBoard, fileCount, totalLines } = data;
151
+ const blocks = findings.filter(f => f.severity === 'block');
152
+ const warns = findings.filter(f => f.severity === 'warn');
153
+ const infos = findings.filter(f => f.severity === 'info');
154
+
155
+ // Group by file
156
+ const byFile = {};
157
+ for (const f of findings) {
158
+ const file = f.file || '(unknown)';
159
+ if (!byFile[file]) byFile[file] = [];
160
+ byFile[file].push(f);
161
+ }
162
+
163
+ // Group by rule
164
+ const byRule = {};
165
+ for (const f of findings) {
166
+ const rule = f.ruleId || f.category || 'other';
167
+ if (!byRule[rule]) byRule[rule] = [];
168
+ byRule[rule].push(f);
169
+ }
170
+
171
+ const severityColor = { block: '#e24b4a', warn: '#ef9f27', info: '#888780' };
172
+ const severityBg = { block: '#fcebeb', warn: '#faeeda', info: '#f1efe8' };
173
+
174
+ return `<!DOCTYPE html>
175
+ <html lang="zh-CN">
176
+ <head>
177
+ <meta charset="UTF-8">
178
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
179
+ <title>DevAssist 检查报告 — ${projectRoot}</title>
180
+ <style>
181
+ * { margin: 0; padding: 0; box-sizing: border-box; }
182
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f3; color: #2c2c2a; line-height: 1.6; }
183
+ .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
184
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 8px; }
185
+ h2 { font-size: 18px; font-weight: 500; margin: 32px 0 16px; padding-bottom: 8px; border-bottom: 1px solid #e0ddd5; }
186
+ .meta { color: #888780; font-size: 13px; margin-bottom: 24px; }
187
+ .stats { display: flex; gap: 16px; margin-bottom: 24px; }
188
+ .stat-card { flex: 1; padding: 20px; border-radius: 12px; text-align: center; }
189
+ .stat-card .num { font-size: 32px; font-weight: 600; }
190
+ .stat-card .label { font-size: 12px; color: #888780; margin-top: 4px; }
191
+ .stat-block { background: #fcebeb; }
192
+ .stat-block .num { color: #e24b4a; }
193
+ .stat-warn { background: #faeeda; }
194
+ .stat-warn .num { color: #ef9f27; }
195
+ .stat-info { background: #f1efe8; }
196
+ .stat-info .num { color: #888780; }
197
+ .stat-files { background: #e6f1fb; }
198
+ .stat-files .num { color: #185fa5; }
199
+ .finding { padding: 12px 16px; margin-bottom: 8px; border-radius: 8px; border-left: 3px solid; }
200
+ .finding-block { background: #fcebeb; border-color: #e24b4a; }
201
+ .finding-warn { background: #faeeda; border-color: #ef9f27; }
202
+ .finding-info { background: #f1efe8; border-color: #888780; }
203
+ .finding .title { font-weight: 500; font-size: 13px; }
204
+ .finding .location { color: #888780; font-size: 12px; margin-top: 2px; }
205
+ .finding .suggestion { color: #5f5e5a; font-size: 12px; margin-top: 4px; }
206
+ .file-group { margin-bottom: 16px; }
207
+ .file-name { font-weight: 500; font-size: 13px; color: #534ab7; margin-bottom: 8px; }
208
+ .debt-table { width: 100%; border-collapse: collapse; font-size: 13px; }
209
+ .debt-table th { text-align: left; padding: 8px 12px; background: #f1efe8; font-weight: 500; }
210
+ .debt-table td { padding: 8px 12px; border-bottom: 1px solid #e0ddd5; }
211
+ .debt-table .priority { font-weight: 600; }
212
+ .debt-table .high { color: #e24b4a; }
213
+ .debt-table .medium { color: #ef9f27; }
214
+ .debt-table .low { color: #888780; }
215
+ .empty { color: #888780; text-align: center; padding: 32px; font-size: 14px; }
216
+ .rule-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; }
217
+ .rule-bar .bar { height: 6px; border-radius: 3px; background: #534ab7; min-width: 2px; }
218
+ .rule-bar .count { color: #888780; min-width: 30px; }
219
+ </style>
220
+ </head>
221
+ <body>
222
+ <div class="container">
223
+ <h1>DevAssist 代码检查报告</h1>
224
+ <div class="meta">项目:${projectRoot} | 生成时间:${new Date(timestamp).toLocaleString('zh-CN')} | ${fileCount} 个文件,${totalLines} 行</div>
225
+
226
+ <div class="stats">
227
+ <div class="stat-card stat-block"><div class="num">${blocks.length}</div><div class="label">阻断</div></div>
228
+ <div class="stat-card stat-warn"><div class="num">${warns.length}</div><div class="label">警告</div></div>
229
+ <div class="stat-card stat-info"><div class="num">${infos.length}</div><div class="label">提示</div></div>
230
+ <div class="stat-card stat-files"><div class="num">${fileCount}</div><div class="label">文件</div></div>
231
+ </div>
232
+
233
+ ${blocks.length > 0 ? `
234
+ <h2>阻断(${blocks.length})</h2>
235
+ ${blocks.map(f => findingHTML(f)).join('')}
236
+ ` : ''}
237
+
238
+ ${warns.length > 0 ? `
239
+ <h2>警告(${warns.length})</h2>
240
+ ${warns.map(f => findingHTML(f)).join('')}
241
+ ` : ''}
242
+
243
+ ${infos.length > 0 ? `
244
+ <h2>提示(${infos.length})</h2>
245
+ ${infos.slice(0, 20).map(f => findingHTML(f)).join('')}
246
+ ${infos.length > 20 ? `<div class="empty">...还有 ${infos.length - 20} 条</div>` : ''}
247
+ ` : ''}
248
+
249
+ ${Object.keys(byFile).length > 0 ? `
250
+ <h2>按文件分组</h2>
251
+ ${Object.entries(byFile).sort((a, b) => b[1].length - a[1].length).map(([file, fileFindings]) => `
252
+ <div class="file-group">
253
+ <div class="file-name">${file} (${fileFindings.length})</div>
254
+ ${fileFindings.map(f => findingHTML(f)).join('')}
255
+ </div>
256
+ `).join('')}
257
+ ` : ''}
258
+
259
+ ${Object.keys(byRule).length > 0 ? `
260
+ <h2>规则触发统计</h2>
261
+ ${Object.entries(byRule).sort((a, b) => b[1].length - a[1].length).map(([rule, ruleFindings]) => {
262
+ const maxCount = Math.max(...Object.values(byRule).map(r => r.length));
263
+ const width = (ruleFindings.length / maxCount) * 200;
264
+ return `<div class="rule-bar"><span style="min-width:200px">${rule}</span><div class="bar" style="width:${width}px"></div><span class="count">${ruleFindings.length}</span></div>`;
265
+ }).join('')}
266
+ ` : ''}
267
+
268
+ ${debtBoard.items && debtBoard.items.length > 0 ? `
269
+ <h2>技术债务(${debtBoard.items.length})</h2>
270
+ <table class="debt-table">
271
+ <thead><tr><th>文件</th><th>问题描述</th><th>严重度</th><th>修复工时</th><th>优先级</th></tr></thead>
272
+ <tbody>
273
+ ${debtBoard.items.slice(0, 15).map(item => {
274
+ const sevLabel = { high: '高', medium: '中', low: '低' }[item.severity] || '低';
275
+ return `
276
+ <tr>
277
+ <td>${item.file || '-'}</td>
278
+ <td>${item.description || item.type || '-'}</td>
279
+ <td class="priority ${item.severity || 'low'}">${sevLabel}</td>
280
+ <td>${item.effort || '-'}</td>
281
+ <td class="priority ${item.priority > 0.5 ? 'high' : item.priority > 0.2 ? 'medium' : 'low'}">${(item.priority || 0).toFixed(2)}</td>
282
+ </tr>
283
+ `;}).join('')}
284
+ </tbody>
285
+ </table>
286
+ ${debtBoard.items.length > 15 ? `<div class="empty">...还有 ${debtBoard.items.length - 15} 条</div>` : ''}
287
+ ` : ''}
288
+
289
+ ${findings.length === 0 ? `<div class="empty">全部检查通过,未发现问题。</div>` : ''}
290
+
291
+ <div style="margin-top:48px;padding-top:16px;border-top:1px solid #e0ddd5;color:#888780;font-size:12px">
292
+ 由 DevAssist Agent v0.1.0 生成 | 共 ${findings.length} 项发现,涉及 ${Object.keys(byFile).length} 个文件
293
+ </div>
294
+ </div>
295
+ </body>
296
+ </html>`;
297
+ }
298
+
299
+ function findingHTML(f) {
300
+ const sev = f.severity || 'info';
301
+ return `<div class="finding finding-${sev}">
302
+ <div class="title">${escapeHTML(f.message || '未知问题')}</div>
303
+ ${f.file ? `<div class="location">${escapeHTML(f.file)}${f.line ? `:${f.line}` : ''}</div>` : ''}
304
+ ${f.suggestion ? `<div class="suggestion">${escapeHTML(f.suggestion)}</div>` : ''}
305
+ </div>`;
306
+ }
307
+
308
+ function escapeHTML(str) {
309
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
310
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * devassist restore - Restore from backup (non-destructive)
3
+ *
4
+ * Key safety feature: shows a pre-restore report first.
5
+ * Does NOT delete files that exist now but weren't in the backup.
6
+ */
7
+
8
+ const { VersionManager } = require('../../modules/dev-time/version-manager');
9
+
10
+ module.exports = function restore(projectRoot, args, ui) {
11
+ const { printHeader, printFinding, c } = ui;
12
+ printHeader('从备份恢复');
13
+
14
+ const vm = new VersionManager(projectRoot);
15
+ const versions = vm.listVersions();
16
+
17
+ if (versions.length === 0) {
18
+ console.log(` ${c.yellow('没有可用备份。')} 请先执行 ${c.bold('devassist backup')}。\n`);
19
+ return;
20
+ }
21
+
22
+ const versionId = args.find(a => !a.startsWith('--'));
23
+
24
+ if (!versionId) {
25
+ console.log(` ${c.gray('可用备份:')}\n`);
26
+ for (const v of versions) {
27
+ console.log(` ${c.blue(v.versionId)} ${c.gray(v.label)} ${c.gray('(' + v.stats.total + ' 个文件, ' + v.stats.changed + ' 变更)')}`);
28
+ }
29
+ console.log(`\n 用法:devassist restore ${c.gray('<版本号>')}\n`);
30
+ return;
31
+ }
32
+
33
+ // Prepare restore (non-destructive report)
34
+ const report = vm.prepareRestore(versionId);
35
+
36
+ console.log(` ${c.bold('备份:')} ${versionId}\n`);
37
+
38
+ if (report.willOverwrite.length > 0) {
39
+ console.log(` ${c.yellow('将被覆盖的文件:')}`);
40
+ for (const f of report.willOverwrite.slice(0, 10)) {
41
+ console.log(` ${c.gray(f)}`);
42
+ }
43
+ if (report.willOverwrite.length > 10) {
44
+ console.log(` ${c.gray('...还有 ' + (report.willOverwrite.length - 10) + ' 个')}`);
45
+ }
46
+ console.log();
47
+ }
48
+
49
+ if (report.willRestore.length > 0) {
50
+ console.log(` ${c.green('将恢复的文件(当前缺失):')}`);
51
+ for (const f of report.willRestore.slice(0, 10)) {
52
+ console.log(` ${c.gray(f)}`);
53
+ }
54
+ console.log();
55
+ }
56
+
57
+ if (report.newFilesNotInBackup.length > 0) {
58
+ console.log(` ${c.red(c.bold('警告:' + report.newFilesNotInBackup.length + ' 个文件当前存在但不在该备份中!'))}`);
59
+ console.log(` ${c.gray('这些文件将被保留(非破坏性恢复)。')}`);
60
+ console.log(` ${c.gray('对应故障 #12:备份恢复删除了新文件。')}\n`);
61
+ for (const f of report.newFilesNotInBackup.slice(0, 10)) {
62
+ console.log(` ${c.gray(f)}`);
63
+ }
64
+ if (report.newFilesNotInBackup.length > 10) {
65
+ console.log(` ${c.gray('...还有 ' + (report.newFilesNotInBackup.length - 10) + ' 个')}`);
66
+ }
67
+ console.log();
68
+ }
69
+
70
+ // Check for --yes flag to skip confirmation
71
+ if (!args.includes('--yes')) {
72
+ console.log(` ${c.gray('添加 ' + c.bold('--yes') + ' 确认执行恢复。')}\n`);
73
+ return;
74
+ }
75
+
76
+ const result = vm.doRestore(versionId);
77
+ console.log(` ${c.green('恢复完成!')} 恢复了 ${result.restored} 个文件,保留了 ${result.newFilesPreserved} 个新文件。\n`);
78
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * devassist schema - Manage schema registry
3
+ *
4
+ * devassist schema import <file.sql> Import DB schema from DDL
5
+ * devassist schema list List registered schemas
6
+ * devassist schema api add <endpoint> --data-key <key> Register API contract
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { SchemaRegistry } = require('../../modules/dev-time/schema-registry');
12
+
13
+ module.exports = function schema(projectRoot, args, ui) {
14
+ const { printHeader, c } = ui;
15
+ const subcommand = args[0];
16
+ const registry = new SchemaRegistry(projectRoot);
17
+
18
+ switch (subcommand) {
19
+ case 'import': {
20
+ printHeader('导入数据库 Schema');
21
+ const file = args[1];
22
+ if (!file) {
23
+ console.log(` 用法:devassist schema import ${c.gray('<文件.sql>')}\n`);
24
+ return;
25
+ }
26
+ const fullPath = path.isAbsolute(file) ? file : path.join(projectRoot, file);
27
+ if (!fs.existsSync(fullPath)) {
28
+ console.log(` ${c.red('文件不存在:')} ${fullPath}\n`);
29
+ return;
30
+ }
31
+ const content = fs.readFileSync(fullPath, 'utf-8');
32
+ const findings = registry.importFromDDL(content);
33
+ const stats = registry.getStats();
34
+ console.log(` ${c.green('已导入')} ${stats.tables} 张表,${stats.totalColumns} 个字段\n`);
35
+ if (findings.length > 0) {
36
+ console.log(` ${c.yellow('检测到 Schema 变更:')}\n`);
37
+ for (const f of findings) {
38
+ console.log(` ${f.severity === 'block' ? c.red('[阻断]') : c.yellow('[警告] ')} ${f.message}\n`);
39
+ }
40
+ }
41
+ break;
42
+ }
43
+ case 'list': {
44
+ printHeader('Schema 注册中心');
45
+ const stats = registry.getStats();
46
+ console.log(` ${c.bold('数据表:')} ${stats.tables}`);
47
+ console.log(` ${c.bold('API 契约:')} ${stats.apiContracts}`);
48
+ console.log(` ${c.bold('字段总数:')} ${stats.totalColumns}\n`);
49
+
50
+ const snapshot = registry.exportSnapshot();
51
+ if (Object.keys(snapshot.schemas).length > 0) {
52
+ console.log(` ${c.bold('已注册的数据表:')}\n`);
53
+ for (const [table, schema] of Object.entries(snapshot.schemas)) {
54
+ console.log(` ${c.blue(table)} ${c.gray('v' + schema.version + ' (' + schema.columns.length + ' 个字段)')}`);
55
+ }
56
+ console.log();
57
+ }
58
+ if (Object.keys(snapshot.apiContracts).length > 0) {
59
+ console.log(` ${c.bold('API 契约:')}\n`);
60
+ for (const [endpoint, contract] of Object.entries(snapshot.apiContracts)) {
61
+ console.log(` ${c.blue(endpoint)} ${c.gray('data-key: ' + (contract.dataKey || 'data.list') + ' v' + contract.version)}`);
62
+ }
63
+ console.log();
64
+ }
65
+ break;
66
+ }
67
+ case 'api': {
68
+ const action = args[1];
69
+ if (action === 'add') {
70
+ printHeader('注册 API 契约');
71
+ const endpoint = args[2];
72
+ const dataKey = getArg(args, '--data-key') || 'data.list';
73
+ if (!endpoint) {
74
+ console.log(` 用法:devassist schema api add ${c.gray('<端点> --data-key <键名>')}\n`);
75
+ return;
76
+ }
77
+ registry.registerAPI(endpoint, { dataKey });
78
+ console.log(` ${c.green('已注册 API 契约:')} ${endpoint}(dataKey: ${dataKey})\n`);
79
+ } else {
80
+ console.log(` 用法:devassist schema api ${c.gray('add <端点> --data-key <键名>')}\n`);
81
+ }
82
+ break;
83
+ }
84
+ default:
85
+ console.log(` 用法:devassist schema ${c.gray('import|list|api')}\n`);
86
+ }
87
+ };
88
+
89
+ function getArg(args, flag) {
90
+ const idx = args.indexOf(flag);
91
+ if (idx >= 0 && args[idx + 1]) return args[idx + 1];
92
+ return null;
93
+ }