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,326 @@
1
+ /**
2
+ * AI Multi-Engine Comparison Report Generator
3
+ *
4
+ * Generates an HTML report showing side-by-side analysis from N AI providers.
5
+ * Highlights agreements and disagreements to help developers make informed fix decisions.
6
+ *
7
+ * Data format:
8
+ * {
9
+ * engines: [{ provider, model, name }],
10
+ * stats: { allAgree, partialAgree, allDisagree, partialAvailable, agreementRate },
11
+ * findings: [{ severity, message, file, line, ruleId, aiAnalyses: [...] }]
12
+ * }
13
+ */
14
+
15
+ // Engine color palette (cycled for N engines)
16
+ const ENGINE_COLORS = [
17
+ { bg: '#e8f0fe', border: '#c5d8f5', tag: '#185fa5', panel: '#f8faff', name: '蓝' },
18
+ { bg: '#fef3e8', border: '#f5d8c5', tag: '#c4722d', panel: '#fffaf8', name: '橙' },
19
+ { bg: '#e8f7ed', border: '#c5e8d0', tag: '#2d9e4e', panel: '#f8fffa', name: '绿' },
20
+ { bg: '#f3e8fe', border: '#e0c5f5', tag: '#7b3fb5', panel: '#fdf8ff', name: '紫' },
21
+ { bg: '#fef8e8', border: '#f5e0c5', tag: '#b5942d', panel: '#fffdf8', name: '金' },
22
+ { bg: '#e8fefe', border: '#c5f0f0', tag: '#2d8e94', panel: '#f8ffff', name: '青' },
23
+ ];
24
+
25
+ function generateComparisonHTML(data) {
26
+ const { projectRoot, timestamp, engines = [], stats = {}, findings = [] } = data;
27
+
28
+ // Only include findings that have at least one analysis
29
+ const analyzedFindings = findings.filter(f =>
30
+ f.aiAnalyses && f.aiAnalyses.some(ea => ea.available && ea.analysis)
31
+ );
32
+
33
+ // Categorize findings
34
+ const allAgree = [];
35
+ const partialAgree = [];
36
+ const allDisagree = [];
37
+ const partialAvailable = [];
38
+
39
+ for (const f of analyzedFindings) {
40
+ if (!f.aiAnalyses) continue;
41
+ const available = f.aiAnalyses.filter(ea => ea.available && ea.analysis);
42
+ if (available.length < 2) {
43
+ partialAvailable.push(f);
44
+ continue;
45
+ }
46
+
47
+ const fpValues = available.map(ea => ea.analysis.isFalsePositive);
48
+ const allSame = fpValues.every(v => v === fpValues[0]);
49
+
50
+ if (available.length === engines.length) {
51
+ if (allSame) {
52
+ allAgree.push(f);
53
+ } else {
54
+ // Check majority
55
+ const trueCount = fpValues.filter(v => v === true).length;
56
+ const falseCount = fpValues.filter(v => v === false).length;
57
+ if (Math.max(trueCount, falseCount) >= 2) {
58
+ partialAgree.push(f);
59
+ } else {
60
+ allDisagree.push(f);
61
+ }
62
+ }
63
+ } else {
64
+ partialAvailable.push(f);
65
+ }
66
+ }
67
+
68
+ const totalAnalyzed = analyzedFindings.length;
69
+ const numEngines = engines.length;
70
+
71
+ // Dynamic grid columns
72
+ const gridCols = `repeat(${numEngines}, 1fr)`;
73
+ const minCardWidth = numEngines > 3 ? '320px' : '1fr';
74
+
75
+ // Engine header cards
76
+ const engineHeaders = engines.map((eng, i) => {
77
+ const color = ENGINE_COLORS[i % ENGINE_COLORS.length];
78
+ return `<div class="engine-card" style="background:${color.bg};border:1px solid ${color.border}">
79
+ <div class="label">${i === 0 ? '主引擎' : '引擎 ' + i}</div>
80
+ <div class="name">${escapeHTML(eng.name)}</div>
81
+ <div class="model">${escapeHTML(eng.provider)} / ${escapeHTML(eng.model)}</div>
82
+ </div>`;
83
+ }).join('');
84
+
85
+ // Stats cards
86
+ const statsHTML = `
87
+ <div class="stat-card stat-agree"><div class="num">${allAgree.length}</div><div class="label">全引擎一致</div></div>
88
+ <div class="stat-card stat-partial"><div class="num">${partialAgree.length}</div><div class="label">部分一致</div></div>
89
+ <div class="stat-card stat-disagree"><div class="num">${allDisagree.length}</div><div class="label">全引擎不一致</div></div>
90
+ <div class="stat-card stat-rate"><div class="num">${stats.agreementRate || '—'}</div><div class="label">一致率</div></div>
91
+ `;
92
+
93
+ return `<!DOCTYPE html>
94
+ <html lang="zh-CN">
95
+ <head>
96
+ <meta charset="UTF-8">
97
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
98
+ <title>AI 多引擎对比报告 — ${escapeHTML(projectRoot)}</title>
99
+ <style>
100
+ * { margin: 0; padding: 0; box-sizing: border-box; }
101
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f3; color: #2c2c2a; line-height: 1.6; }
102
+ .container { max-width: 1400px; margin: 0 auto; padding: 40px 24px; }
103
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 8px; }
104
+ h2 { font-size: 18px; font-weight: 500; margin: 32px 0 16px; padding-bottom: 8px; border-bottom: 1px solid #e0ddd5; }
105
+ h3 { font-size: 15px; font-weight: 500; margin: 20px 0 12px; }
106
+ .meta { color: #888780; font-size: 13px; margin-bottom: 24px; }
107
+
108
+ /* Engine headers */
109
+ .engines { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
110
+ .engine-card { flex: 1; min-width: 160px; padding: 14px 18px; border-radius: 12px; }
111
+ .engine-card .label { font-size: 12px; color: #888780; margin-bottom: 4px; }
112
+ .engine-card .name { font-size: 16px; font-weight: 600; }
113
+ .engine-card .model { font-size: 12px; color: #888780; margin-top: 2px; }
114
+
115
+ /* Stats */
116
+ .stats { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
117
+ .stat-card { flex: 1; min-width: 100px; padding: 16px; border-radius: 10px; text-align: center; }
118
+ .stat-card .num { font-size: 28px; font-weight: 600; }
119
+ .stat-card .label { font-size: 11px; color: #888780; margin-top: 4px; }
120
+ .stat-agree { background: #e8f7ed; } .stat-agree .num { color: #2d9e4e; }
121
+ .stat-partial { background: #fef3e8; } .stat-partial .num { color: #e8943a; }
122
+ .stat-disagree { background: #fcebeb; } .stat-disagree .num { color: #e24b4a; }
123
+ .stat-rate { background: #f1efe8; } .stat-rate .num { color: #534ab7; font-size: 20px; }
124
+
125
+ /* Finding card */
126
+ .finding-card { background: #fff; border-radius: 12px; margin-bottom: 16px; overflow: hidden; border: 1px solid #e0ddd5; }
127
+ .finding-header { padding: 12px 16px; border-bottom: 1px solid #e0ddd5; }
128
+ .finding-header .sev { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-right: 8px; }
129
+ .sev-block { background: #fcebeb; color: #e24b4a; }
130
+ .sev-warn { background: #faeeda; color: #ef9f27; }
131
+ .finding-header .msg { font-weight: 500; font-size: 14px; }
132
+ .finding-header .loc { color: #888780; font-size: 12px; margin-top: 2px; }
133
+
134
+ /* Multi-engine grid */
135
+ .comparison-grid { display: grid; grid-template-columns: ${gridCols}; gap: 0; }
136
+ @media (max-width: 900px) {
137
+ .comparison-grid { grid-template-columns: 1fr; }
138
+ }
139
+ .analysis-panel { padding: 14px; border-right: 1px solid #e8e5dd; }
140
+ .analysis-panel:last-child { border-right: none; }
141
+ .analysis-panel .engine-tag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px; display: inline-block; margin-bottom: 8px; }
142
+ .analysis-panel .field { margin-bottom: 8px; }
143
+ .analysis-panel .field-label { font-size: 11px; color: #888780; font-weight: 500; }
144
+ .analysis-panel .field-value { font-size: 13px; margin-top: 1px; }
145
+ .analysis-panel .code { background: #f1efe8; padding: 8px 12px; border-radius: 6px; font-family: 'SF Mono', Consolas, monospace; font-size: 12px; margin-top: 4px; white-space: pre-wrap; word-break: break-word; }
146
+ .analysis-panel .confidence { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
147
+ .conf-high { background: #e8f7ed; color: #2d9e4e; }
148
+ .conf-mid { background: #faeeda; color: #ef9f27; }
149
+ .conf-low { background: #fcebeb; color: #e24b4a; }
150
+ .analysis-unavailable { color: #aaa; font-style: italic; font-size: 13px; }
151
+
152
+ /* Agreement badge */
153
+ .agreement-badge { padding: 6px 12px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
154
+ .badge-agree { background: #e8f7ed; color: #2d9e4e; }
155
+ .badge-partial { background: #fef3e8; color: #e8943a; }
156
+ .badge-disagree { background: #fcebeb; color: #e24b4a; }
157
+ .badge-deferred { background: #fff3cd; color: #856404; border: 1px solid #ffe08a; }
158
+ .badge-fix { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
159
+ .badge-review { background: #e8f0fe; color: #185fa5; border: 1px solid #c5d8f5; }
160
+ .finding-deferred { border-left: 4px solid #ffc107; }
161
+
162
+ /* Section tabs */
163
+ .section-tab { display: inline-block; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; margin-right: 8px; }
164
+ .tab-agree { background: #e8f7ed; color: #2d9e4e; }
165
+ .tab-partial { background: #fef3e8; color: #e8943a; }
166
+ .tab-disagree { background: #fcebeb; color: #e24b4a; }
167
+
168
+ .empty { color: #888780; text-align: center; padding: 32px; font-size: 14px; }
169
+ .tip { background: #f8faff; border: 1px solid #d0d8f0; border-radius: 8px; padding: 16px 20px; margin: 24px 0; font-size: 13px; color: #444; }
170
+ .tip strong { color: #185fa5; }
171
+ </style>
172
+ </head>
173
+ <body>
174
+ <div class="container">
175
+ <h1>AI ${numEngines} 引擎对比分析报告</h1>
176
+ <div class="meta">项目:${escapeHTML(projectRoot)} | 生成时间:${new Date(timestamp).toLocaleString('zh-CN')} | 共分析 ${totalAnalyzed} 个发现 | ${numEngines} 个引擎</div>
177
+
178
+ <div class="engines">
179
+ ${engineHeaders}
180
+ </div>
181
+
182
+ <div class="stats">
183
+ ${statsHTML}
184
+ </div>
185
+
186
+ ${allDisagree.length > 0 ? `
187
+ <h2><span class="section-tab tab-disagree">⏳ 全引擎不一致</span> 已标记为待观察(${allDisagree.length})</h2>
188
+ <div class="tip"><strong>策略:</strong>这些发现引擎间判断存在分歧,<strong>暂不修复</strong>。已自动备注为"待观察",等实际运行验证后再决定是否处理。</div>
189
+ ${allDisagree.map(f => findingComparisonHTML(f, engines, 'deferred')).join('')}
190
+ ` : ''}
191
+
192
+ ${partialAgree.length > 0 ? `
193
+ <h2><span class="section-tab tab-partial">◐ 部分一致</span> 多数引擎一致(${partialAgree.length})</h2>
194
+ <div class="tip"><strong>建议:</strong>这些发现多数引擎判断一致,参考少数意见后决定是否修复。也可标记为待观察。</div>
195
+ ${partialAgree.map(f => findingComparisonHTML(f, engines, 'review')).join('')}
196
+ ` : ''}
197
+
198
+ ${allAgree.length > 0 ? `
199
+ <h2><span class="section-tab tab-agree">✓ 全引擎一致</span> 可信度高,建议优先处理(${allAgree.length})</h2>
200
+ <div class="tip"><strong>建议:</strong>这些发现所有引擎判断一致,可信度高,建议优先处理非误报项。</div>
201
+ ${allAgree.map(f => findingComparisonHTML(f, engines, 'fix')).join('')}
202
+ ` : ''}
203
+
204
+ ${partialAvailable.length > 0 ? `
205
+ <h2 style="color:#888780">○ 部分引擎未返回(${partialAvailable.length})</h2>
206
+ ${partialAvailable.map(f => findingComparisonHTML(f, engines)).join('')}
207
+ ` : ''}
208
+
209
+ ${analyzedFindings.length === 0 ? `<div class="empty">无 AI 分析结果。</div>` : ''}
210
+
211
+ <div style="margin-top:48px;padding-top:16px;border-top:1px solid #e0ddd5;color:#888780;font-size:12px">
212
+ 由 DevAssist 多引擎对比分析生成 | ${engines.map(e => escapeHTML(e.name)).join(' · ')}
213
+ </div>
214
+ </div>
215
+ </body>
216
+ </html>`;
217
+ }
218
+
219
+ function findingComparisonHTML(f, engines, mode) {
220
+ const sev = f.severity || 'info';
221
+ const sevClass = sev === 'block' ? 'sev-block' : 'sev-warn';
222
+ const sevLabel = sev === 'block' ? '阻断' : '警告';
223
+
224
+ // Mode-based action badge
225
+ let actionBadge = '';
226
+ if (mode === 'deferred') {
227
+ actionBadge = '<div class="agreement-badge badge-deferred">⏳ 待观察 — 暂不修复,等运行时验证</div>';
228
+ } else if (mode === 'fix') {
229
+ actionBadge = '<div class="agreement-badge badge-fix">🔧 建议修复 — 全引擎一致认定</div>';
230
+ } else if (mode === 'review') {
231
+ actionBadge = '<div class="agreement-badge badge-review">👀 可酌情处理 — 参考多数引擎意见</div>';
232
+ }
233
+
234
+ // Determine agreement badge
235
+ let badge = '';
236
+ if (f.aiAnalyses) {
237
+ const available = f.aiAnalyses.filter(ea => ea.available && ea.analysis);
238
+ if (available.length >= 2) {
239
+ const fpValues = available.map(ea => ea.analysis.isFalsePositive);
240
+ const allSame = fpValues.every(v => v === fpValues[0]);
241
+ if (available.length === engines.length) {
242
+ if (allSame) {
243
+ badge = '<div class="agreement-badge badge-agree">✓ 全引擎一致</div>';
244
+ } else {
245
+ const trueCount = fpValues.filter(v => v === true).length;
246
+ const falseCount = fpValues.filter(v => v === false).length;
247
+ if (Math.max(trueCount, falseCount) >= 2) {
248
+ badge = '<div class="agreement-badge badge-partial">◐ 部分一致</div>';
249
+ } else {
250
+ badge = '<div class="agreement-badge badge-disagree">⏳ 全引擎不一致 — 已标记待观察</div>';
251
+ }
252
+ }
253
+ } else {
254
+ badge = '<div class="agreement-badge badge-partial">○ 部分引擎未返回</div>';
255
+ }
256
+ } else if (available.length === 1) {
257
+ badge = '<div class="agreement-badge badge-partial">○ 仅单引擎</div>';
258
+ }
259
+ }
260
+
261
+ // Generate N panels
262
+ const panels = engines.map((eng, i) => {
263
+ const color = ENGINE_COLORS[i % ENGINE_COLORS.length];
264
+ const ea = f.aiAnalyses?.[i];
265
+ return `<div class="analysis-panel" style="background:${color.panel}">
266
+ <span class="engine-tag" style="background:${color.bg};color:${color.tag}">${escapeHTML(eng.name)}</span>
267
+ ${analysisHTML(ea)}
268
+ </div>`;
269
+ }).join('');
270
+
271
+ return `<div class="finding-card${mode === 'deferred' ? ' finding-deferred' : ''}">
272
+ <div class="finding-header">
273
+ <span class="sev ${sevClass}">${sevLabel}</span>
274
+ <span class="msg">${escapeHTML(f.message || '未知问题')}</span>
275
+ <div class="loc">${escapeHTML(f.file || '')}${f.line ? ':' + f.line : ''}${f.ruleId ? ' | ' + escapeHTML(f.ruleId) : ''}</div>
276
+ ${actionBadge}
277
+ ${badge}
278
+ </div>
279
+ <div class="comparison-grid">
280
+ ${panels}
281
+ </div>
282
+ </div>`;
283
+ }
284
+
285
+ function analysisHTML(ea) {
286
+ if (!ea || !ea.available || !ea.analysis) {
287
+ const reason = ea?.error ? `(${escapeHTML(ea.error)})` : '';
288
+ return `<div class="analysis-unavailable">分析不可用${reason}</div>`;
289
+ }
290
+
291
+ const a = ea.analysis;
292
+ let html = '';
293
+
294
+ if (a.rootCause) {
295
+ html += `<div class="field"><div class="field-label">根因分析</div><div class="field-value">${escapeHTML(a.rootCause)}</div></div>`;
296
+ }
297
+
298
+ if (a.isFalsePositive !== undefined) {
299
+ const fpText = a.isFalsePositive ? '是(误报)' : '否(真实问题)';
300
+ const fpColor = a.isFalsePositive ? '#888780' : '#e24b4a';
301
+ html += `<div class="field"><div class="field-label">是否误报</div><div class="field-value" style="color:${fpColor};font-weight:500">${fpText}</div></div>`;
302
+ }
303
+
304
+ if (a.fix) {
305
+ html += `<div class="field"><div class="field-label">修复建议</div><div class="field-value">${escapeHTML(a.fix)}</div></div>`;
306
+ }
307
+
308
+ if (a.codeExample) {
309
+ html += `<div class="field"><div class="field-label">代码示例</div><div class="code">${escapeHTML(a.codeExample)}</div></div>`;
310
+ }
311
+
312
+ if (a.confidence !== undefined) {
313
+ const pct = (a.confidence * 100).toFixed(0);
314
+ const confClass = a.confidence >= 0.7 ? 'conf-high' : a.confidence >= 0.4 ? 'conf-mid' : 'conf-low';
315
+ html += `<div class="field"><div class="field-label">置信度</div><span class="confidence ${confClass}">${pct}%</span></div>`;
316
+ }
317
+
318
+ return html || '<div class="analysis-unavailable">无分析内容</div>';
319
+ }
320
+
321
+ function escapeHTML(str) {
322
+ if (str === null || str === undefined) return '';
323
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
324
+ }
325
+
326
+ module.exports = { generateComparisonHTML };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * devassist convention - Manage project conventions
3
+ *
4
+ * devassist convention add --id <id> --rule "<rule text>" [--severity block|warn|info] [--forbid <regex>] [--require <regex>]
5
+ * devassist convention list
6
+ * devassist convention remove <id>
7
+ */
8
+
9
+ const { ConventionStore } = require('../../modules/dev-time/convention-store');
10
+
11
+ module.exports = function convention(projectRoot, args, ui) {
12
+ const { printHeader, c } = ui;
13
+ const subcommand = args[0];
14
+ const store = new ConventionStore(projectRoot);
15
+
16
+ switch (subcommand) {
17
+ case 'add': {
18
+ printHeader('添加约定');
19
+ const id = getArg(args, '--id');
20
+ const rule = getArg(args, '--rule');
21
+ if (!id || !rule) {
22
+ console.log(` 用法:devassist convention add ${c.gray('--id <标识> --rule "<规则描述>"')} [--severity block|warn|info] [--forbid <正则>] [--require <正则>]\n`);
23
+ return;
24
+ }
25
+ const conv = {
26
+ id,
27
+ rule,
28
+ severity: getArg(args, '--severity') || 'warn',
29
+ description: getArg(args, '--desc') || '',
30
+ appliesTo: ['js', 'ts', 'jsx', 'tsx'],
31
+ };
32
+ const forbid = getArg(args, '--forbid');
33
+ const require = getArg(args, '--require');
34
+ if (forbid) conv.forbid = forbid;
35
+ if (require) conv.require = require;
36
+ // Remove _example flag — this is now a real convention
37
+ delete conv._example;
38
+ store.add(conv);
39
+ console.log(` ${c.green('已添加约定:')} ${id}\n`);
40
+ break;
41
+ }
42
+ case 'list': {
43
+ printHeader('项目约定');
44
+ const { conventions, adrs } = store.list();
45
+ if (conventions.length === 0) {
46
+ console.log(` ${c.yellow('尚未注册任何约定。')}\n`);
47
+ } else {
48
+ for (const conv of conventions) {
49
+ const sev = conv.severity === 'block' ? c.red('[阻断]') : conv.severity === 'warn' ? c.yellow('[警告]') : c.gray('[提示]');
50
+ const example = conv._example ? c.gray(' (示例)') : '';
51
+ console.log(` ${sev} ${c.bold(conv.id)}${example}`);
52
+ console.log(` ${conv.rule}`);
53
+ if (conv.description) console.log(` ${c.gray(conv.description)}`);
54
+ if (conv.forbid) console.log(` ${c.gray('禁止:/' + conv.forbid + '/')}`);
55
+ if (conv.require) console.log(` ${c.gray('要求:/' + conv.require + '/')}`);
56
+ console.log();
57
+ }
58
+ }
59
+ if (adrs.length > 0) {
60
+ console.log(` ${c.bold('架构决策记录:')}\n`);
61
+ for (const adr of adrs) {
62
+ console.log(` ${c.blue(adr.id)}: ${adr.title || adr.decision}`);
63
+ console.log(` ${c.gray(adr.date)}\n`);
64
+ }
65
+ }
66
+ break;
67
+ }
68
+ case 'remove': {
69
+ printHeader('移除约定');
70
+ const id = args[1];
71
+ if (!id) {
72
+ console.log(` 用法:devassist convention remove <标识>\n`);
73
+ return;
74
+ }
75
+ if (store.remove(id)) {
76
+ console.log(` ${c.green('已移除:')} ${id}\n`);
77
+ } else {
78
+ console.log(` ${c.yellow('未找到:')} ${id}\n`);
79
+ }
80
+ break;
81
+ }
82
+ default:
83
+ console.log(` 用法:devassist convention ${c.gray('add|list|remove')}\n`);
84
+ }
85
+ };
86
+
87
+ function getArg(args, flag) {
88
+ const idx = args.indexOf(flag);
89
+ if (idx >= 0 && args[idx + 1]) return args[idx + 1];
90
+ return null;
91
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * devassist debt - Show tech debt board
3
+ */
4
+
5
+ const { TechDebtTracker } = require('../../modules/dev-time/tech-debt-tracker');
6
+
7
+ module.exports = function debt(projectRoot, args, ui) {
8
+ const { printHeader, c } = ui;
9
+ printHeader('技术债务看板');
10
+
11
+ const tracker = new TechDebtTracker(projectRoot);
12
+
13
+ // Run scan if --scan flag or no history
14
+ if (args.includes('--scan') || tracker.history.length === 0) {
15
+ console.log(` ${c.gray('正在扫描项目债务指标...')}\n`);
16
+ const snapshot = tracker.scan();
17
+ console.log(` ${c.green('扫描完成:')} ${snapshot.totalDebt} 个债务项(${snapshot.highSeverity} 个高严重度)\n`);
18
+ }
19
+
20
+ const board = tracker.getBoard();
21
+
22
+ if (board.topItems === undefined) {
23
+ console.log(` ${c.yellow(board.message || '暂无扫描数据。')}\n`);
24
+ console.log(` 运行 ${c.bold('devassist debt --scan')} 扫描你的项目。\n`);
25
+ return;
26
+ }
27
+
28
+ console.log(` ${c.bold('债务项总数:')} ${board.totalDebt}`);
29
+ console.log(` ${c.bold('高严重度:')} ${board.highSeverity}`);
30
+ console.log(` ${c.bold('趋势:')} ${board.trend}`);
31
+ console.log(` ${c.bold('最近扫描:')} ${board.timestamp}\n`);
32
+
33
+ if (board.topItems && board.topItems.length > 0) {
34
+ console.log(` ${c.bold('最高优先级项:')}\n`);
35
+ for (const item of board.topItems) {
36
+ const sev = item.severity === 'high' ? c.red('[高]') :
37
+ item.severity === 'medium' ? c.yellow('[中]') :
38
+ c.gray('[低]');
39
+ console.log(` ${sev} ${item.description}`);
40
+ console.log(` ${c.gray('类型:' + item.type + ' | 工时:' + item.effort + ' | 优先级:' + item.priority)}`);
41
+ if (item.failureLink) {
42
+ console.log(` ${c.gray('关联:' + item.failureLink)}`);
43
+ }
44
+ console.log();
45
+ }
46
+ }
47
+
48
+ console.log(` ${c.gray('运行 ' + c.bold('devassist debt --scan') + ' 刷新数据。')}\n`);
49
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * devassist deploy --check - Pre-deployment validation
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { PreDeployGuard } = require('../../modules/dev-time/pre-deploy-guard');
8
+ const { VersionManager } = require('../../modules/dev-time/version-manager');
9
+
10
+ module.exports = async function deploy(projectRoot, args, ui) {
11
+ const { printHeader, printFinding, c } = ui;
12
+
13
+ if (!args.includes('--check')) {
14
+ console.log(` 用法:devassist deploy ${c.gray('--check')} [--port <n>]\n`);
15
+ return;
16
+ }
17
+
18
+ printHeader('部署前验证');
19
+
20
+ const guard = new PreDeployGuard(projectRoot);
21
+
22
+ // Parse port from args
23
+ let port = null;
24
+ const portIdx = args.indexOf('--port');
25
+ if (portIdx >= 0 && args[portIdx + 1]) {
26
+ port = parseInt(args[portIdx + 1], 10);
27
+ }
28
+
29
+ // Also try to detect port from project config
30
+ if (!port) {
31
+ const packageJsonPath = path.join(projectRoot, 'package.json');
32
+ if (fs.existsSync(packageJsonPath)) {
33
+ try {
34
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
35
+ const startScript = pkg.scripts?.start || '';
36
+ const portMatch = startScript.match(/PORT=(\d+)/);
37
+ if (portMatch) port = parseInt(portMatch[1], 10);
38
+ } catch (_) {}
39
+ }
40
+ }
41
+
42
+ // Also check for .env or config files
43
+ if (!port) {
44
+ const envPath = path.join(projectRoot, '.env');
45
+ if (fs.existsSync(envPath)) {
46
+ const envContent = fs.readFileSync(envPath, 'utf-8');
47
+ const portMatch = envContent.match(/PORT=(\d+)/);
48
+ if (portMatch) port = parseInt(portMatch[1], 10);
49
+ }
50
+ }
51
+
52
+ console.log(` ${c.gray('检查端口:')} ${port || '(未指定——请用 --port <n>)'}`);
53
+ console.log(` ${c.gray('检查 nginx 配置...')}`);
54
+ console.log(` ${c.gray('检查 MIME 类型...')}`);
55
+ console.log(` ${c.gray('检查部署清单...')}`);
56
+ console.log(` ${c.gray('检查缓存失效...')}`);
57
+ console.log();
58
+
59
+ // Generate deploy manifest if not exists
60
+ const versionManager = new VersionManager(projectRoot);
61
+ const manifest = versionManager.generateDeployManifest();
62
+ console.log(` ${c.green('已生成')} deploy-manifest.json(${manifest.totalFiles} 个文件)\n`);
63
+
64
+ // Run all checks
65
+ const findings = await guard.runAll({ port });
66
+
67
+ if (findings.length === 0) {
68
+ console.log(` ${c.green('全部部署前检查通过!')} 可以安全部署。\n`);
69
+ return;
70
+ }
71
+
72
+ for (const f of findings) {
73
+ printFinding(f);
74
+ }
75
+
76
+ const blocks = findings.filter(f => f.severity === 'block');
77
+ const warnings = findings.filter(f => f.severity === 'warn');
78
+
79
+ console.log(c.gray(' ────────────────────────────────────────'));
80
+ console.log(` 结果:${c.red(blocks.length + ' 阻断')}, ${c.yellow(warnings.length + ' 警告')}, ${findings.length - blocks.length - warnings.length} 提示`);
81
+
82
+ if (blocks.length > 0) {
83
+ console.log(`\n ${c.red(c.bold('部署已阻断。'))}` + ' 请修复阻断问题后再部署。\n');
84
+ process.exit(1);
85
+ } else if (warnings.length > 0) {
86
+ console.log(`\n ${c.yellow('带警告部署。')} 请审查警告问题。\n`);
87
+ }
88
+ };