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,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist init - Initialize project
|
|
3
|
+
*
|
|
4
|
+
* Creates:
|
|
5
|
+
* .devassist/
|
|
6
|
+
* ├── conventions.json (empty, with examples commented out)
|
|
7
|
+
* ├── schema-registry.json (empty)
|
|
8
|
+
* ├── tech-debt.json (empty)
|
|
9
|
+
* ├── version-manifest.json (empty)
|
|
10
|
+
* ├── deploy-manifest.json (generated)
|
|
11
|
+
* ├── ai-config.json (template)
|
|
12
|
+
* └── dependency-graph.json (generated on first check)
|
|
13
|
+
*
|
|
14
|
+
* With --base-code flag, also installs:
|
|
15
|
+
* lib/
|
|
16
|
+
* ├── core/ (EventBus, FSM, Logger, RuleEngine)
|
|
17
|
+
* ├── runtime/ (ToolRegistry, ChannelAgent, ChannelMiddleware,
|
|
18
|
+
* │ InfrastructureGuard, NotificationCenter, SchemaGatekeeper)
|
|
19
|
+
* └── index.js (unified exports)
|
|
20
|
+
* server.js (template server with everything wired up)
|
|
21
|
+
*
|
|
22
|
+
* Also creates a pre-commit hook if git is detected.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
module.exports = function init(projectRoot, args, ui) {
|
|
29
|
+
const { printHeader, c } = ui;
|
|
30
|
+
printHeader('初始化项目');
|
|
31
|
+
|
|
32
|
+
const configDir = path.join(projectRoot, '.devassist');
|
|
33
|
+
const hasExisting = fs.existsSync(configDir);
|
|
34
|
+
const installBaseCode = args.includes('--base-code') || args.includes('--full');
|
|
35
|
+
|
|
36
|
+
if (hasExisting && !args.includes('--force')) {
|
|
37
|
+
console.log(` ${c.yellow('项目已初始化。')} 使用 ${c.bold('--force')} 覆盖现有配置。\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create config directory
|
|
42
|
+
if (!fs.existsSync(configDir)) {
|
|
43
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. conventions.json
|
|
47
|
+
const conventions = {
|
|
48
|
+
conventions: [
|
|
49
|
+
// Example convention (commented out by using _ prefix)
|
|
50
|
+
{
|
|
51
|
+
_example: true,
|
|
52
|
+
id: 'naming-list-key',
|
|
53
|
+
category: 'api',
|
|
54
|
+
rule: '列表响应数据必须使用键名 "list",不用 "items" 或 "records"',
|
|
55
|
+
description: '防止三变体问题:data.list/data.items/data.records',
|
|
56
|
+
appliesTo: ['js', 'ts', 'json'],
|
|
57
|
+
severity: 'block',
|
|
58
|
+
forbid: 'data\\.(items|records)\\s*[=:]',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
adrs: [],
|
|
62
|
+
};
|
|
63
|
+
fs.writeFileSync(path.join(configDir, 'conventions.json'), JSON.stringify(conventions, null, 2), 'utf-8');
|
|
64
|
+
console.log(` ${c.green('已创建')} conventions.json`);
|
|
65
|
+
|
|
66
|
+
// 2. schema-registry.json
|
|
67
|
+
const schemaRegistry = { schemas: {}, apiContracts: {} };
|
|
68
|
+
fs.writeFileSync(path.join(configDir, 'schema-registry.json'), JSON.stringify(schemaRegistry, null, 2), 'utf-8');
|
|
69
|
+
console.log(` ${c.green('已创建')} schema-registry.json`);
|
|
70
|
+
|
|
71
|
+
// 3. tech-debt.json
|
|
72
|
+
const techDebt = { history: [], thresholds: {
|
|
73
|
+
maxFileLines: 800,
|
|
74
|
+
maxFunctionLines: 150,
|
|
75
|
+
maxCyclomaticComplexity: 20,
|
|
76
|
+
maxDuplicateBlocks: 3,
|
|
77
|
+
maxCouplingScore: 10,
|
|
78
|
+
}};
|
|
79
|
+
fs.writeFileSync(path.join(configDir, 'tech-debt.json'), JSON.stringify(techDebt, null, 2), 'utf-8');
|
|
80
|
+
console.log(` ${c.green('已创建')} tech-debt.json`);
|
|
81
|
+
|
|
82
|
+
// 4. version-manifest.json
|
|
83
|
+
fs.writeFileSync(path.join(configDir, 'version-manifest.json'), '[]', 'utf-8');
|
|
84
|
+
console.log(` ${c.green('已创建')} version-manifest.json`);
|
|
85
|
+
|
|
86
|
+
// 4.5. config.json (project-level configuration)
|
|
87
|
+
const projectConfig = {
|
|
88
|
+
maxFileLines: 800,
|
|
89
|
+
todoThreshold: 3,
|
|
90
|
+
consoleLogThreshold: 5,
|
|
91
|
+
rules: {},
|
|
92
|
+
ai: {
|
|
93
|
+
autoAnalyze: false,
|
|
94
|
+
maxFindingsToAI: 20,
|
|
95
|
+
},
|
|
96
|
+
_comment: '在此自定义规则阈值、启用/禁用规则、覆盖严重级别。例如: rules: { "cq-magic-numbers": { enabled: false } }',
|
|
97
|
+
};
|
|
98
|
+
fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify(projectConfig, null, 2), 'utf-8');
|
|
99
|
+
console.log(` ${c.green('已创建')} config.json`);
|
|
100
|
+
|
|
101
|
+
// 5. ai-config.json
|
|
102
|
+
const aiConfig = {
|
|
103
|
+
provider: '', // 'qwen' | 'openai' | 'claude' | 'hunyuan'
|
|
104
|
+
apiKey: '', // 或设置环境变量: DASHSCOPE_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / HUNYUAN_API_KEY
|
|
105
|
+
model: '',
|
|
106
|
+
baseUrl: '',
|
|
107
|
+
enabled: false,
|
|
108
|
+
_comment: '设置 provider 和 apiKey,然后将 enabled 设为 true。也可使用环境变量。',
|
|
109
|
+
_envVars: {
|
|
110
|
+
qwen: 'DASHSCOPE_API_KEY',
|
|
111
|
+
openai: 'OPENAI_API_KEY',
|
|
112
|
+
claude: 'ANTHROPIC_API_KEY',
|
|
113
|
+
hunyuan: 'HUNYUAN_API_KEY',
|
|
114
|
+
},
|
|
115
|
+
_models: {
|
|
116
|
+
qwen: 'qwen-plus',
|
|
117
|
+
openai: 'gpt-4o-mini',
|
|
118
|
+
claude: 'claude-sonnet-4-20250514',
|
|
119
|
+
hunyuan: 'hy3-preview',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
fs.writeFileSync(path.join(configDir, 'ai-config.json'), JSON.stringify(aiConfig, null, 2), 'utf-8');
|
|
123
|
+
console.log(` ${c.green('已创建')} ai-config.json`);
|
|
124
|
+
|
|
125
|
+
// 6. deploy-manifest.json (placeholder)
|
|
126
|
+
const deployManifest = { generatedAt: '', totalFiles: 0, files: [] };
|
|
127
|
+
fs.writeFileSync(path.join(configDir, 'deploy-manifest.json'), JSON.stringify(deployManifest, null, 2), 'utf-8');
|
|
128
|
+
console.log(` ${c.green('已创建')} deploy-manifest.json`);
|
|
129
|
+
|
|
130
|
+
// 7. Create backups directory
|
|
131
|
+
const backupDir = path.join(configDir, 'backups');
|
|
132
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
133
|
+
console.log(` ${c.green('已创建')} backups/`);
|
|
134
|
+
|
|
135
|
+
// 7.5. Create reports directory
|
|
136
|
+
const reportDir = path.join(configDir, 'reports');
|
|
137
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
138
|
+
console.log(` ${c.green('已创建')} reports/`);
|
|
139
|
+
|
|
140
|
+
// 7.6. Create history directory
|
|
141
|
+
const historyDir = path.join(configDir, 'history');
|
|
142
|
+
if (!fs.existsSync(historyDir)) fs.mkdirSync(historyDir, { recursive: true });
|
|
143
|
+
console.log(` ${c.green('已创建')} history/`);
|
|
144
|
+
|
|
145
|
+
// 7.5. Create .devassistignore
|
|
146
|
+
const ignorePath = path.join(projectRoot, '.devassistignore');
|
|
147
|
+
if (!fs.existsSync(ignorePath) || args.includes('--force')) {
|
|
148
|
+
const ignoreContent = `# DevAssist 忽略文件 — 类似 .gitignore
|
|
149
|
+
# 此处列出的目录在扫描时将被跳过。
|
|
150
|
+
# 每行一个模式。目录以 / 结尾,通配符使用 *。
|
|
151
|
+
|
|
152
|
+
# 备份/归档目录
|
|
153
|
+
*_backup/
|
|
154
|
+
*_old/
|
|
155
|
+
*_archive/
|
|
156
|
+
*backup*/
|
|
157
|
+
|
|
158
|
+
# 依赖
|
|
159
|
+
node_modules/
|
|
160
|
+
|
|
161
|
+
# 构建产物
|
|
162
|
+
dist/
|
|
163
|
+
build/
|
|
164
|
+
|
|
165
|
+
# 无脚本的设计文档(取消注释可跳过所有 HTML)
|
|
166
|
+
# *.html
|
|
167
|
+
`;
|
|
168
|
+
fs.writeFileSync(ignorePath, ignoreContent, 'utf-8');
|
|
169
|
+
console.log(` ${c.green('已创建')} .devassistignore`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 8. Git pre-commit hook
|
|
173
|
+
const gitDir = path.join(projectRoot, '.git');
|
|
174
|
+
if (fs.existsSync(gitDir)) {
|
|
175
|
+
const hookDir = path.join(gitDir, 'hooks');
|
|
176
|
+
if (!fs.existsSync(hookDir)) fs.mkdirSync(hookDir, { recursive: true });
|
|
177
|
+
const hookPath = path.join(hookDir, 'pre-commit');
|
|
178
|
+
const hookContent = `#!/bin/sh
|
|
179
|
+
# DevAssist pre-commit hook
|
|
180
|
+
# Runs devassist check before allowing a commit
|
|
181
|
+
echo "[DevAssist] Running pre-commit checks..."
|
|
182
|
+
node "${path.join(__dirname, '..', '..', '..', 'bin', 'devassist.js')}" check --pre-commit
|
|
183
|
+
if [ $? -ne 0 ]; then
|
|
184
|
+
echo "[DevAssist] Commit blocked by check failures. Fix issues or use --no-verify to bypass."
|
|
185
|
+
exit 1
|
|
186
|
+
fi
|
|
187
|
+
exit 0
|
|
188
|
+
`;
|
|
189
|
+
fs.writeFileSync(hookPath, hookContent, 'utf-8');
|
|
190
|
+
try { fs.chmodSync(hookPath, 0o755); } catch (_) {}
|
|
191
|
+
console.log(` ${c.green('已创建')} .git/hooks/pre-commit`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 9. Install base code (Layer 2 runtime infrastructure) if requested
|
|
195
|
+
if (installBaseCode) {
|
|
196
|
+
console.log(`\n ${c.cyan('正在安装基础代码(运行时基础设施)...')}`);
|
|
197
|
+
installBaseCodeModules(projectRoot, c);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`\n ${c.bold(c.green('项目初始化完成!'))}\n`);
|
|
201
|
+
console.log(` ${c.gray('后续步骤:')}`);
|
|
202
|
+
if (installBaseCode) {
|
|
203
|
+
console.log(` 1. 查看 lib/server.js 了解模板服务器配置`);
|
|
204
|
+
console.log(` 2. 编辑 .devassist/conventions.json 添加项目约定`);
|
|
205
|
+
console.log(` 3. 编辑 .devassist/ai-config.json 启用 AI 分析(可选)`);
|
|
206
|
+
console.log(` 4. 运行 ${c.bold('devassist check')} 扫描代码库`);
|
|
207
|
+
console.log(` 5. 运行 ${c.bold('devassist schema import <file.sql>')} 注册数据库模式\n`);
|
|
208
|
+
} else {
|
|
209
|
+
console.log(` 1. 编辑 .devassist/conventions.json 添加项目约定`);
|
|
210
|
+
console.log(` 2. 编辑 .devassist/ai-config.json 启用 AI 分析(可选)`);
|
|
211
|
+
console.log(` 3. 编辑 .devassist/config.json 自定义规则阈值(可选)`);
|
|
212
|
+
console.log(` 4. 运行 ${c.bold('devassist check')} 扫描代码库`);
|
|
213
|
+
console.log(` 5. 运行 ${c.bold('devassist schema import <file.sql>')} 注册数据库模式`);
|
|
214
|
+
console.log(` 6. 运行 ${c.bold('devassist init --base-code')} 安装运行时基础设施\n`);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Install base code modules into the target project's lib/ directory.
|
|
220
|
+
* Copies core and runtime modules, adjusting require paths.
|
|
221
|
+
*/
|
|
222
|
+
function installBaseCodeModules(projectRoot, c) {
|
|
223
|
+
const devassistRoot = path.join(__dirname, '..', '..', '..');
|
|
224
|
+
const libDir = path.join(projectRoot, 'lib');
|
|
225
|
+
const coreDir = path.join(libDir, 'core');
|
|
226
|
+
const runtimeDir = path.join(libDir, 'runtime');
|
|
227
|
+
|
|
228
|
+
// Create directories
|
|
229
|
+
for (const dir of [coreDir, runtimeDir]) {
|
|
230
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Copy core modules (no path adjustment needed — they have no relative requires)
|
|
234
|
+
const coreFiles = ['event-bus.js', 'fsm.js', 'logger.js', 'rule-engine.js'];
|
|
235
|
+
for (const file of coreFiles) {
|
|
236
|
+
const src = path.join(devassistRoot, 'src', 'core', file);
|
|
237
|
+
if (fs.existsSync(src)) {
|
|
238
|
+
fs.copyFileSync(src, path.join(coreDir, file));
|
|
239
|
+
console.log(` ${c.green('已创建')} lib/core/${file}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Copy runtime modules (adjust require paths: ../../core/ -> ../core/)
|
|
244
|
+
const runtimeFiles = [
|
|
245
|
+
'tool-registry.js', 'channel-agent.js', 'channel-middleware.js',
|
|
246
|
+
'infrastructure-guard.js', 'notification-center.js', 'schema-gatekeeper.js',
|
|
247
|
+
'index.js',
|
|
248
|
+
];
|
|
249
|
+
for (const file of runtimeFiles) {
|
|
250
|
+
const src = path.join(devassistRoot, 'src', 'modules', 'runtime', file);
|
|
251
|
+
if (fs.existsSync(src)) {
|
|
252
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
253
|
+
// Adjust require paths for target project structure
|
|
254
|
+
content = content.replace(/require\('\.\.\/\.\.\/core\//g, "require('../core/");
|
|
255
|
+
content = content.replace(/require\('\.\.\/\.\.\/modules\//g, "require('../");
|
|
256
|
+
fs.writeFileSync(path.join(runtimeDir, file), content, 'utf-8');
|
|
257
|
+
console.log(` ${c.green('已创建')} lib/runtime/${file}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create lib/index.js (unified entry point)
|
|
262
|
+
const libIndex = `/**
|
|
263
|
+
* Base code entry point — installed by devassist init --base-code
|
|
264
|
+
*
|
|
265
|
+
* This gives your project the zero-upgrade architecture foundation:
|
|
266
|
+
* - ToolRegistry for dynamic tool registration
|
|
267
|
+
* - ChannelAgent for protocol-level communication
|
|
268
|
+
* - ChannelMiddleware for request/response processing
|
|
269
|
+
* - InfrastructureGuard for runtime health monitoring
|
|
270
|
+
* - NotificationCenter for multi-channel alerting
|
|
271
|
+
* - SchemaGatekeeper for runtime data validation
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
// Core
|
|
276
|
+
EventBus: require('./core/event-bus'),
|
|
277
|
+
FSM: require('./core/fsm'),
|
|
278
|
+
Logger: require('./core/logger'),
|
|
279
|
+
|
|
280
|
+
// Runtime
|
|
281
|
+
...require('./runtime'),
|
|
282
|
+
};
|
|
283
|
+
`;
|
|
284
|
+
fs.writeFileSync(path.join(libDir, 'index.js'), libIndex, 'utf-8');
|
|
285
|
+
console.log(` ${c.green('已创建')} lib/index.js`);
|
|
286
|
+
|
|
287
|
+
// Create server.js template
|
|
288
|
+
const serverTemplate = `/**
|
|
289
|
+
* Server template — installed by devassist init --base-code
|
|
290
|
+
*
|
|
291
|
+
* This template shows how to wire up all the base code modules.
|
|
292
|
+
* Customize it for your project's needs.
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
const http = require('http');
|
|
296
|
+
const {
|
|
297
|
+
EventBus: { bus },
|
|
298
|
+
FSM: { FSM, STATES },
|
|
299
|
+
Logger: { Logger },
|
|
300
|
+
ToolRegistry: { registry, MODULE_TYPES },
|
|
301
|
+
ChannelAgent: { ChannelAgent, PROTOCOL_VERSION, PRIORITY },
|
|
302
|
+
ChannelMiddleware: { compose, requestLogger, authGuard, rateLimiter, schemaValidator, errorHandler },
|
|
303
|
+
InfrastructureGuard: { InfrastructureGuard, HEALTH_STATE },
|
|
304
|
+
NotificationCenter: { center },
|
|
305
|
+
SchemaGatekeeper: { gatekeeper },
|
|
306
|
+
} = require('./lib');
|
|
307
|
+
|
|
308
|
+
const log = new Logger('Server');
|
|
309
|
+
|
|
310
|
+
// --- Initialize InfrastructureGuard ---
|
|
311
|
+
const guard = new InfrastructureGuard();
|
|
312
|
+
guard.addHttpProbe({ url: 'http://localhost:8080/health', interval: 30000 });
|
|
313
|
+
guard.addMemoryProbe({ maxHeapMB: 512, interval: 30000 });
|
|
314
|
+
guard.addDiskProbe({ path: process.cwd(), minFreeMB: 500, interval: 60000 });
|
|
315
|
+
guard.start();
|
|
316
|
+
|
|
317
|
+
// --- Initialize ChannelAgent ---
|
|
318
|
+
const agent = new ChannelAgent({ registry, secret: process.env.CHANNEL_SECRET || null });
|
|
319
|
+
agent.init();
|
|
320
|
+
|
|
321
|
+
// --- Register your tools here ---
|
|
322
|
+
// Example:
|
|
323
|
+
// agent.route('chat.send', 'chat-tool-id', { priority: PRIORITY.NORMAL });
|
|
324
|
+
// agent.route('voice.tts', 'voice-tts-tool-id', { priority: PRIORITY.NORMAL });
|
|
325
|
+
|
|
326
|
+
// --- Build middleware chain ---
|
|
327
|
+
const middleware = compose([
|
|
328
|
+
errorHandler(),
|
|
329
|
+
requestLogger(),
|
|
330
|
+
corsHandler?.() || ((ctx, next) => next()),
|
|
331
|
+
rateLimiter({ windowMs: 60000, max: 100 }),
|
|
332
|
+
authGuard({ exclude: ['heartbeat.ping', 'channel.state', 'config.greetings'] }),
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
// --- HTTP Server ---
|
|
336
|
+
const server = http.createServer(async (req, res) => {
|
|
337
|
+
if (req.method === 'OPTIONS') {
|
|
338
|
+
res.writeHead(204, {
|
|
339
|
+
'Access-Control-Allow-Origin': '*',
|
|
340
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
341
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
342
|
+
});
|
|
343
|
+
res.end();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Health endpoint
|
|
348
|
+
if (req.url === '/health') {
|
|
349
|
+
const status = guard.getStatus();
|
|
350
|
+
res.writeHead(status.state === HEALTH_STATE.HEALTHY ? 200 : 503, { 'Content-Type': 'application/json' });
|
|
351
|
+
res.end(JSON.stringify(status));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Tool manifest endpoint
|
|
356
|
+
if (req.url === '/api/tools') {
|
|
357
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
358
|
+
res.end(JSON.stringify(registry.getManifest()));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Main API endpoint
|
|
363
|
+
if (req.url === '/api' || req.url.startsWith('/api/')) {
|
|
364
|
+
let body = '';
|
|
365
|
+
req.on('data', chunk => body += chunk);
|
|
366
|
+
req.on('end', async () => {
|
|
367
|
+
let parsedBody;
|
|
368
|
+
try {
|
|
369
|
+
parsedBody = body ? JSON.parse(body) : {};
|
|
370
|
+
} catch (e) {
|
|
371
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
372
|
+
res.end(JSON.stringify({ code: 400, message: 'Invalid JSON' }));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const ctx = {
|
|
377
|
+
body: parsedBody,
|
|
378
|
+
ip: req.socket.remoteAddress,
|
|
379
|
+
headers: req.headers,
|
|
380
|
+
url: req.url,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
await middleware(ctx, async () => {
|
|
384
|
+
// If middleware set a response (e.g. auth failure), use it
|
|
385
|
+
if (ctx.response) {
|
|
386
|
+
res.writeHead(ctx.response.code === 200 ? 200 : ctx.response.code, { 'Content-Type': 'application/json' });
|
|
387
|
+
res.end(JSON.stringify(ctx.response));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Hand off to ChannelAgent
|
|
392
|
+
const result = await agent.handleRequest(parsedBody, ctx);
|
|
393
|
+
res.writeHead(result.code === 200 ? 200 : result.code, { 'Content-Type': 'application/json' });
|
|
394
|
+
res.end(JSON.stringify(result));
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ code: 404, message: 'Not found' }));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const PORT = process.env.PORT || 8080;
|
|
405
|
+
server.listen(PORT, () => {
|
|
406
|
+
log.info(\`Server running on port \${PORT}\`);
|
|
407
|
+
log.info(\`Protocol: \${PROTOCOL_VERSION}\`);
|
|
408
|
+
log.info(\`Tools registered: \${registry.getStats().totalTools}\`);
|
|
409
|
+
center.info('Server started', \`Listening on port \${PORT}\`, 'server');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Graceful shutdown
|
|
413
|
+
process.on('SIGTERM', () => {
|
|
414
|
+
log.info('SIGTERM received, shutting down...');
|
|
415
|
+
guard.stop();
|
|
416
|
+
agent.destroy();
|
|
417
|
+
server.close(() => process.exit(0));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
process.on('SIGINT', () => {
|
|
421
|
+
log.info('SIGINT received, shutting down...');
|
|
422
|
+
guard.stop();
|
|
423
|
+
agent.destroy();
|
|
424
|
+
server.close(() => process.exit(0));
|
|
425
|
+
});
|
|
426
|
+
`;
|
|
427
|
+
fs.writeFileSync(path.join(projectRoot, 'server.js'), serverTemplate, 'utf-8');
|
|
428
|
+
console.log(` ${c.green('已创建')} server.js`);
|
|
429
|
+
|
|
430
|
+
console.log(`\n ${c.cyan('基础代码安装完成!')} lib/ 目录 10 个文件 + server.js 模板`);
|
|
431
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist inject - 生成约定注入文件
|
|
3
|
+
*
|
|
4
|
+
* 核心差异化能力:让 Trae/WorkBuddy/Cursor 等 AI Agent 在写代码时
|
|
5
|
+
* 自动获取项目约定,从根源防止"AI遗忘早期约定"(32/88 故障根因)。
|
|
6
|
+
*
|
|
7
|
+
* devassist inject 生成 .devassist/CONVENTIONS.md
|
|
8
|
+
* devassist inject --format cursor 生成 .cursorrules
|
|
9
|
+
* devassist inject --format trae 生成 .traerules
|
|
10
|
+
* devassist inject --format markdown 生成 .devassist/CONVENTIONS.md
|
|
11
|
+
* devassist inject --format stdout 输出到终端(用于管道)
|
|
12
|
+
* devassist inject --format all 生成所有格式
|
|
13
|
+
*
|
|
14
|
+
* 原理:AI Agent 在开始写代码前会读取项目根目录的规则文件
|
|
15
|
+
* (.cursorrules / .traerules / CONVENTIONS.md 等)。
|
|
16
|
+
* 本命令将 .devassist/conventions.json 中的约定自动同步到这些文件,
|
|
17
|
+
* 确保约定始终是最新的。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { ConventionStore } = require('../../modules/dev-time/convention-store');
|
|
23
|
+
|
|
24
|
+
module.exports = function inject(projectRoot, args, ui) {
|
|
25
|
+
const { printHeader, c } = ui;
|
|
26
|
+
printHeader('约定注入');
|
|
27
|
+
|
|
28
|
+
const format = getArg(args, '--format') || 'markdown';
|
|
29
|
+
const store = new ConventionStore(projectRoot);
|
|
30
|
+
const { conventions, adrs } = store.list();
|
|
31
|
+
|
|
32
|
+
// Filter out example conventions
|
|
33
|
+
const realConventions = conventions.filter(conv => !conv._example);
|
|
34
|
+
|
|
35
|
+
if (realConventions.length === 0 && adrs.length === 0) {
|
|
36
|
+
console.log(` ${c.yellow('尚未注册任何约定。')}\n`);
|
|
37
|
+
console.log(` ${c.gray('先添加约定:')}`);
|
|
38
|
+
console.log(` devassist convention add --id <标识> --rule "<规则描述>"\n`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(` ${c.gray('约定数量:')} ${realConventions.length} 条约定,${adrs.length} 条 ADR`);
|
|
43
|
+
console.log(` ${c.gray('输出格式:')} ${format}\n`);
|
|
44
|
+
|
|
45
|
+
const content = generateContent(realConventions, adrs, format);
|
|
46
|
+
|
|
47
|
+
if (format === 'stdout') {
|
|
48
|
+
console.log(content);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (format === 'all') {
|
|
53
|
+
// Generate all formats
|
|
54
|
+
const formats = ['markdown', 'cursor', 'trae'];
|
|
55
|
+
for (const fmt of formats) {
|
|
56
|
+
const fmtContent = generateContent(realConventions, adrs, fmt);
|
|
57
|
+
const filePath = getFilePath(projectRoot, fmt);
|
|
58
|
+
writeToFile(filePath, fmtContent, c);
|
|
59
|
+
}
|
|
60
|
+
console.log(`\n ${c.green('全部约定文件已生成!')}\n`);
|
|
61
|
+
console.log(` ${c.gray('AI Agent 现在会自动读取这些约定。')}\n`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const filePath = getFilePath(projectRoot, format);
|
|
66
|
+
writeToFile(filePath, content, c);
|
|
67
|
+
|
|
68
|
+
console.log(`\n ${c.green('约定文件已生成!')}\n`);
|
|
69
|
+
|
|
70
|
+
// Print usage hint based on format
|
|
71
|
+
switch (format) {
|
|
72
|
+
case 'cursor':
|
|
73
|
+
console.log(` ${c.gray('Cursor 会自动读取项目根目录的 .cursorrules 文件。')}`);
|
|
74
|
+
break;
|
|
75
|
+
case 'trae':
|
|
76
|
+
console.log(` ${c.gray('Trae 会自动读取项目根目录的 .traerules 文件。')}`);
|
|
77
|
+
break;
|
|
78
|
+
case 'markdown':
|
|
79
|
+
console.log(` ${c.gray('WorkBuddy / Trae / 其他 AI Agent 会读取 .devassist/CONVENTIONS.md。')}`);
|
|
80
|
+
console.log(` ${c.gray('建议在项目根目录的 README 中引用此文件。')}`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n ${c.gray('提示:每次更新约定后重新运行此命令以同步文件。')}`);
|
|
85
|
+
console.log(` ${c.gray(' 可在 git pre-commit hook 中自动执行。')}\n`);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function generateContent(conventions, adrs, format) {
|
|
89
|
+
switch (format) {
|
|
90
|
+
case 'cursor':
|
|
91
|
+
return generateCursorRules(conventions, adrs);
|
|
92
|
+
case 'trae':
|
|
93
|
+
return generateTraeRules(conventions, adrs);
|
|
94
|
+
case 'markdown':
|
|
95
|
+
case 'stdout':
|
|
96
|
+
return generateMarkdown(conventions, adrs);
|
|
97
|
+
default:
|
|
98
|
+
return generateMarkdown(conventions, adrs);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getFilePath(projectRoot, format) {
|
|
103
|
+
switch (format) {
|
|
104
|
+
case 'cursor':
|
|
105
|
+
return path.join(projectRoot, '.cursorrules');
|
|
106
|
+
case 'trae':
|
|
107
|
+
return path.join(projectRoot, '.traerules');
|
|
108
|
+
case 'markdown':
|
|
109
|
+
return path.join(projectRoot, '.devassist', 'CONVENTIONS.md');
|
|
110
|
+
default:
|
|
111
|
+
return path.join(projectRoot, '.devassist', 'CONVENTIONS.md');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeToFile(filePath, content, c) {
|
|
116
|
+
const dir = path.dirname(filePath);
|
|
117
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
119
|
+
const relPath = path.relative(process.cwd(), filePath);
|
|
120
|
+
console.log(` ${c.green('已生成:')} ${relPath}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 生成 Markdown 格式的约定文档
|
|
125
|
+
*/
|
|
126
|
+
function generateMarkdown(conventions, adrs) {
|
|
127
|
+
let md = `# 项目约定(必须遵守)\n\n`;
|
|
128
|
+
md += `> 本文件由 DevAssist 自动生成,请勿手动编辑。\n`;
|
|
129
|
+
md += `> 修改约定请使用:devassist convention add --id <标识> --rule "<规则>"\n`;
|
|
130
|
+
md += `> 然后重新运行:devassist inject\n\n`;
|
|
131
|
+
|
|
132
|
+
// Group conventions by category
|
|
133
|
+
const categories = {};
|
|
134
|
+
for (const conv of conventions) {
|
|
135
|
+
const cat = conv.category || 'general';
|
|
136
|
+
if (!categories[cat]) categories[cat] = [];
|
|
137
|
+
categories[cat].push(conv);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const [cat, convs] of Object.entries(categories)) {
|
|
141
|
+
const catName = CATEGORY_NAMES[cat] || cat;
|
|
142
|
+
md += `## ${catName}\n\n`;
|
|
143
|
+
for (const conv of convs) {
|
|
144
|
+
const sevIcon = conv.severity === 'block' ? '🚫' : conv.severity === 'warn' ? '⚠️' : 'ℹ️';
|
|
145
|
+
md += `### ${sevIcon} ${conv.id}\n\n`;
|
|
146
|
+
md += `**规则:** ${conv.rule}\n\n`;
|
|
147
|
+
if (conv.description) md += `**说明:** ${conv.description}\n\n`;
|
|
148
|
+
if (conv.examples && conv.examples.length > 0) {
|
|
149
|
+
md += `**示例:**\n`;
|
|
150
|
+
for (const ex of conv.examples) md += `- ${ex}\n`;
|
|
151
|
+
md += `\n`;
|
|
152
|
+
}
|
|
153
|
+
if (conv.forbid) md += `**禁止模式:** \`${conv.forbid}\`\n\n`;
|
|
154
|
+
if (conv.require) md += `**要求模式:** \`${conv.require}\`\n\n`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (adrs.length > 0) {
|
|
159
|
+
md += `## 架构决策记录(ADR)\n\n`;
|
|
160
|
+
for (const adr of adrs) {
|
|
161
|
+
md += `### ${adr.id}: ${adr.title || adr.decision}\n\n`;
|
|
162
|
+
if (adr.context) md += `**背景:** ${adr.context}\n\n`;
|
|
163
|
+
if (adr.decision) md += `**决策:** ${adr.decision}\n\n`;
|
|
164
|
+
if (adr.consequences) md += `**后果:** ${adr.consequences}\n\n`;
|
|
165
|
+
md += `*记录时间:${adr.date}*\n\n`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
md += `---\n\n`;
|
|
170
|
+
md += `*最后更新:${new Date().toISOString().split('T')[0]} | 由 DevAssist 生成*\n`;
|
|
171
|
+
|
|
172
|
+
return md;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 生成 .cursorrules 格式
|
|
177
|
+
* Cursor 读取项目根目录的 .cursorrules 文件作为 AI 上下文
|
|
178
|
+
*/
|
|
179
|
+
function generateCursorRules(conventions, adrs) {
|
|
180
|
+
let rules = `# 项目约定 - AI Agent 必须遵守\n\n`;
|
|
181
|
+
rules += `## 重要:以下约定是项目的硬性规则,违反将导致 bug。\n`;
|
|
182
|
+
rules += `## 在生成或修改代码时,必须检查是否符合以下约定。\n\n`;
|
|
183
|
+
|
|
184
|
+
const categories = {};
|
|
185
|
+
for (const conv of conventions) {
|
|
186
|
+
const cat = conv.category || 'general';
|
|
187
|
+
if (!categories[cat]) categories[cat] = [];
|
|
188
|
+
categories[cat].push(conv);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const [cat, convs] of Object.entries(categories)) {
|
|
192
|
+
const catName = CATEGORY_NAMES[cat] || cat;
|
|
193
|
+
rules += `### ${catName}\n\n`;
|
|
194
|
+
for (const conv of convs) {
|
|
195
|
+
const prefix = conv.severity === 'block' ? '[必须]' : conv.severity === 'warn' ? '[建议]' : '[提示]';
|
|
196
|
+
rules += `- ${prefix} **${conv.id}**: ${conv.rule}\n`;
|
|
197
|
+
if (conv.description) rules += ` - 说明: ${conv.description}\n`;
|
|
198
|
+
if (conv.forbid) rules += ` - 禁止: /${conv.forbid}/\n`;
|
|
199
|
+
if (conv.require) rules += ` - 要求: /${conv.require}/\n`;
|
|
200
|
+
}
|
|
201
|
+
rules += `\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (adrs.length > 0) {
|
|
205
|
+
rules += `### 架构决策记录\n\n`;
|
|
206
|
+
for (const adr of adrs) {
|
|
207
|
+
rules += `- **${adr.id}**: ${adr.decision}\n`;
|
|
208
|
+
if (adr.context) rules += ` - 背景: ${adr.context}\n`;
|
|
209
|
+
}
|
|
210
|
+
rules += `\n`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
rules += `## 通用规则\n\n`;
|
|
214
|
+
rules += `1. 修改代码前,先理解当前文件的完整上下文,不要只看局部\n`;
|
|
215
|
+
rules += `2. 不要大幅缩减文件行数(AI 覆盖是 88 条故障中的 #1 根因)\n`;
|
|
216
|
+
rules += `3. 修改函数签名前,搜索所有调用方\n`;
|
|
217
|
+
rules += `4. 保持 API 响应格式一致(字段名、结构)\n`;
|
|
218
|
+
rules += `5. 所有 async 调用必须有错误处理\n`;
|
|
219
|
+
rules += `6. 不要引入新的外部依赖,除非明确讨论过\n\n`;
|
|
220
|
+
rules += `# 由 DevAssist 自动生成 | ${new Date().toISOString().split('T')[0]}\n`;
|
|
221
|
+
|
|
222
|
+
return rules;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 生成 .traerules 格式
|
|
227
|
+
* Trae 读取项目根目录的 .traerules 文件
|
|
228
|
+
*/
|
|
229
|
+
function generateTraeRules(conventions, adrs) {
|
|
230
|
+
let rules = generateCursorRules(conventions, adrs);
|
|
231
|
+
// Trae-specific additions
|
|
232
|
+
rules += `\n## Trae 专用规则\n\n`;
|
|
233
|
+
rules += `- 在生成代码后,运行 \`devassist check\` 验证是否符合约定\n`;
|
|
234
|
+
rules += `- 如果 devassist check 报告 BLOCK 级别问题,必须修复后再提交\n`;
|
|
235
|
+
rules += `- 修改文件前,先运行 \`devassist diff --staged\` 查看变更影响\n`;
|
|
236
|
+
return rules;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const CATEGORY_NAMES = {
|
|
240
|
+
'api': 'API 约定',
|
|
241
|
+
'naming': '命名约定',
|
|
242
|
+
'architecture': '架构约定',
|
|
243
|
+
'code-style': '代码风格',
|
|
244
|
+
'security': '安全约定',
|
|
245
|
+
'database': '数据库约定',
|
|
246
|
+
'error-handling': '错误处理约定',
|
|
247
|
+
'general': '通用约定',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function getArg(args, flag) {
|
|
251
|
+
const idx = args.indexOf(flag);
|
|
252
|
+
if (idx >= 0 && args[idx + 1]) return args[idx + 1];
|
|
253
|
+
return null;
|
|
254
|
+
}
|