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,329 @@
1
+ /**
2
+ * ToolRegistry - Dynamic tool registration center (零升级架构核心)
3
+ *
4
+ * Design from 零升级架构设计.md:
5
+ * - 3 module types: resident (built-in), dynamic (hot-loaded), remote (external)
6
+ * - 8 standard interfaces: register, unregister, get, list, invoke,
7
+ * healthCheck, reload, getManifest
8
+ * - Each tool has: id, name, version, type, category, schema, handler
9
+ *
10
+ * This is the runtime base code that gets installed into projects via
11
+ * `devassist init`. It provides the zero-upgrade architecture foundation:
12
+ * new features are added as tool modules, never requiring app recompilation.
13
+ */
14
+
15
+ const { bus } = require('../../core/event-bus');
16
+ const { Logger } = require('../../core/logger');
17
+
18
+ const log = new Logger('ToolRegistry');
19
+
20
+ const MODULE_TYPES = {
21
+ RESIDENT: 'resident', // Built-in, always loaded
22
+ DYNAMIC: 'dynamic', // Hot-loaded at runtime
23
+ REMOTE: 'remote', // External service proxy
24
+ };
25
+
26
+ /**
27
+ * Standard tool schema definition.
28
+ * Every tool module must export an object with these fields.
29
+ */
30
+ const TOOL_INTERFACE = {
31
+ id: 'string', // Unique identifier (e.g. 'weather.query')
32
+ name: 'string', // Human-readable name
33
+ version: 'string', // Semver version
34
+ type: 'string', // MODULE_TYPES
35
+ category: 'string', // Grouping (e.g. 'chat', 'voice', 'config')
36
+ schema: 'object', // { input: JSONSchema, output: JSONSchema }
37
+ handler: 'function', // async (input, ctx) => output
38
+ init: 'function?', // async () => {} called on register
39
+ destroy: 'function?', // async () => {} called on unregister
40
+ healthCheck: 'function?', // async () => { healthy, details }
41
+ };
42
+
43
+ class ToolRegistry {
44
+ constructor() {
45
+ this._tools = new Map();
46
+ this._categories = new Map(); // category -> Set of tool ids
47
+ this._invocationCount = new Map();
48
+ this._errorCount = new Map();
49
+ }
50
+
51
+ /**
52
+ * Register a tool module.
53
+ * Validates the tool interface, calls init() if present.
54
+ * @param {object} tool - Tool module conforming to TOOL_INTERFACE
55
+ * @returns {Promise<void>}
56
+ */
57
+ async register(tool) {
58
+ // --- Validate interface ---
59
+ const required = ['id', 'name', 'version', 'type', 'category', 'schema', 'handler'];
60
+ for (const field of required) {
61
+ if (tool[field] === undefined || tool[field] === null) {
62
+ throw new Error(`Tool registration failed: missing required field '${field}'`);
63
+ }
64
+ }
65
+ if (typeof tool.handler !== 'function') {
66
+ throw new Error(`Tool '${tool.id}': handler must be a function`);
67
+ }
68
+ if (!Object.values(MODULE_TYPES).includes(tool.type)) {
69
+ throw new Error(`Tool '${tool.id}': invalid type '${tool.type}'. Must be one of: ${Object.values(MODULE_TYPES).join(', ')}`);
70
+ }
71
+ if (this._tools.has(tool.id)) {
72
+ throw new Error(`Tool '${tool.id}' is already registered. Unregister it first.`);
73
+ }
74
+
75
+ // --- Validate schema structure ---
76
+ if (!tool.schema.input || !tool.schema.output) {
77
+ throw new Error(`Tool '${tool.id}': schema must have 'input' and 'output'`);
78
+ }
79
+
80
+ // --- Register ---
81
+ this._tools.set(tool.id, {
82
+ ...tool,
83
+ registeredAt: Date.now(),
84
+ lastInvokedAt: null,
85
+ lastErrorAt: null,
86
+ });
87
+
88
+ // Category index
89
+ if (!this._categories.has(tool.category)) {
90
+ this._categories.set(tool.category, new Set());
91
+ }
92
+ this._categories.get(tool.category).add(tool.id);
93
+
94
+ // Init counters
95
+ this._invocationCount.set(tool.id, 0);
96
+ this._errorCount.set(tool.id, 0);
97
+
98
+ // Call init() if present
99
+ if (typeof tool.init === 'function') {
100
+ try {
101
+ await tool.init();
102
+ log.info(`工具已注册并初始化:${tool.id} v${tool.version} (${tool.type})`);
103
+ } catch (err) {
104
+ // Rollback registration on init failure
105
+ this._tools.delete(tool.id);
106
+ this._categories.get(tool.category)?.delete(tool.id);
107
+ this._invocationCount.delete(tool.id);
108
+ this._errorCount.delete(tool.id);
109
+ throw new Error(`Tool '${tool.id}' init() failed: ${err.message}`);
110
+ }
111
+ } else {
112
+ log.info(`工具已注册:${tool.id} v${tool.version} (${tool.type})`);
113
+ }
114
+
115
+ bus.emit('tool:registered', { id: tool.id, version: tool.version, type: tool.type });
116
+ }
117
+
118
+ /**
119
+ * Unregister a tool module.
120
+ * Calls destroy() if present.
121
+ */
122
+ async unregister(toolId) {
123
+ const tool = this._tools.get(toolId);
124
+ if (!tool) {
125
+ throw new Error(`Tool not found: ${toolId}`);
126
+ }
127
+
128
+ if (typeof tool.destroy === 'function') {
129
+ try {
130
+ await tool.destroy();
131
+ } catch (err) {
132
+ log.warn(`工具 '${toolId}' destroy() 错误:${err.message}`);
133
+ }
134
+ }
135
+
136
+ this._tools.delete(toolId);
137
+ this._categories.get(tool.category)?.delete(toolId);
138
+ this._invocationCount.delete(toolId);
139
+ this._errorCount.delete(toolId);
140
+
141
+ log.info(`工具已注销:${toolId}`);
142
+ bus.emit('tool:unregistered', { id: toolId });
143
+ }
144
+
145
+ /**
146
+ * Get a tool by id (without invoking).
147
+ */
148
+ get(toolId) {
149
+ return this._tools.get(toolId) || null;
150
+ }
151
+
152
+ /**
153
+ * List all registered tools.
154
+ * @param {object} opts - { category, type } filters
155
+ */
156
+ list(opts = {}) {
157
+ let tools = Array.from(this._tools.values());
158
+ if (opts.category) {
159
+ tools = tools.filter(t => t.category === opts.category);
160
+ }
161
+ if (opts.type) {
162
+ tools = tools.filter(t => t.type === opts.type);
163
+ }
164
+ return tools.map(t => ({
165
+ id: t.id,
166
+ name: t.name,
167
+ version: t.version,
168
+ type: t.type,
169
+ category: t.category,
170
+ registeredAt: t.registeredAt,
171
+ lastInvokedAt: t.lastInvokedAt,
172
+ invocations: this._invocationCount.get(t.id) || 0,
173
+ errors: this._errorCount.get(t.id) || 0,
174
+ }));
175
+ }
176
+
177
+ /**
178
+ * Invoke a tool by id.
179
+ * This is the main execution path — all tool calls go through here.
180
+ *
181
+ * @param {string} toolId - Tool identifier
182
+ * @param {object} input - Input data (validated against tool.schema.input)
183
+ * @param {object} ctx - Request context (token, user, requestId, etc.)
184
+ * @returns {Promise<object>} Tool output (validated against tool.schema.output)
185
+ */
186
+ async invoke(toolId, input = {}, ctx = {}) {
187
+ const tool = this._tools.get(toolId);
188
+ if (!tool) {
189
+ throw new Error(`Tool not found: ${toolId}`);
190
+ }
191
+
192
+ const startTime = Date.now();
193
+ this._invocationCount.set(toolId, (this._invocationCount.get(toolId) || 0) + 1);
194
+ tool.lastInvokedAt = startTime;
195
+
196
+ try {
197
+ // --- Input validation (lightweight — SchemaGatekeeper does deep validation) ---
198
+ if (tool.schema.input && tool.schema.input.required) {
199
+ for (const field of tool.schema.input.required) {
200
+ if (input[field] === undefined || input[field] === null) {
201
+ throw new Error(`Missing required input field: '${field}' for tool '${toolId}'`);
202
+ }
203
+ }
204
+ }
205
+
206
+ // --- Invoke handler ---
207
+ const result = await tool.handler(input, ctx);
208
+
209
+ const elapsed = Date.now() - startTime;
210
+ bus.emit('tool:invoked', {
211
+ id: toolId,
212
+ success: true,
213
+ elapsed,
214
+ ctx: { requestId: ctx.requestId },
215
+ });
216
+
217
+ return result;
218
+
219
+ } catch (err) {
220
+ this._errorCount.set(toolId, (this._errorCount.get(toolId) || 0) + 1);
221
+ tool.lastErrorAt = Date.now();
222
+
223
+ const elapsed = Date.now() - startTime;
224
+ bus.emit('tool:invoked', {
225
+ id: toolId,
226
+ success: false,
227
+ elapsed,
228
+ error: err.message,
229
+ ctx: { requestId: ctx.requestId },
230
+ });
231
+
232
+ log.error(`工具 '${toolId}' 调用失败:${err.message}`);
233
+ throw err;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Health check for all or specific tools.
239
+ * @param {string} toolId - If provided, check only this tool
240
+ * @returns {Promise<object>} { healthy: boolean, tools: [{ id, healthy, details }] }
241
+ */
242
+ async healthCheck(toolId) {
243
+ const tools = toolId
244
+ ? [this._tools.get(toolId)].filter(Boolean)
245
+ : Array.from(this._tools.values());
246
+
247
+ const results = [];
248
+ let allHealthy = true;
249
+
250
+ for (const tool of tools) {
251
+ try {
252
+ if (typeof tool.healthCheck === 'function') {
253
+ const result = await tool.healthCheck();
254
+ results.push({ id: tool.id, healthy: result.healthy, details: result.details || {} });
255
+ if (!result.healthy) allHealthy = false;
256
+ } else {
257
+ results.push({ id: tool.id, healthy: true, details: { note: 'no healthCheck defined' } });
258
+ }
259
+ } catch (err) {
260
+ results.push({ id: tool.id, healthy: false, details: { error: err.message } });
261
+ allHealthy = false;
262
+ }
263
+ }
264
+
265
+ return { healthy: allHealthy, tools: results };
266
+ }
267
+
268
+ /**
269
+ * Reload a dynamic tool (unregister + re-register).
270
+ * Only works for 'dynamic' type tools.
271
+ */
272
+ async reload(toolId) {
273
+ const tool = this._tools.get(toolId);
274
+ if (!tool) {
275
+ throw new Error(`Tool not found: ${toolId}`);
276
+ }
277
+ if (tool.type !== MODULE_TYPES.DYNAMIC) {
278
+ throw new Error(`Tool '${toolId}' is type '${tool.type}', only 'dynamic' tools can be reloaded`);
279
+ }
280
+
281
+ log.info(`正在重新加载工具:${toolId}`);
282
+ const toolDef = { ...tool };
283
+ delete toolDef.registeredAt;
284
+ delete toolDef.lastInvokedAt;
285
+ delete toolDef.lastErrorAt;
286
+
287
+ await this.unregister(toolId);
288
+ await this.register(toolDef);
289
+ bus.emit('tool:reloaded', { id: toolId });
290
+ }
291
+
292
+ /**
293
+ * Get a manifest of all tools (for client discovery).
294
+ * Returns a compact list suitable for sending to clients.
295
+ */
296
+ getManifest() {
297
+ return this.list().map(t => ({
298
+ id: t.id,
299
+ name: t.name,
300
+ version: t.version,
301
+ category: t.category,
302
+ type: t.type,
303
+ }));
304
+ }
305
+
306
+ /**
307
+ * Get statistics for monitoring.
308
+ */
309
+ getStats() {
310
+ const tools = Array.from(this._tools.values());
311
+ return {
312
+ totalTools: tools.length,
313
+ byType: Object.values(MODULE_TYPES).reduce((acc, type) => {
314
+ acc[type] = tools.filter(t => t.type === type).length;
315
+ return acc;
316
+ }, {}),
317
+ byCategory: Object.fromEntries(
318
+ Array.from(this._categories.entries()).map(([cat, set]) => [cat, set.size])
319
+ ),
320
+ totalInvocations: Array.from(this._invocationCount.values()).reduce((a, b) => a + b, 0),
321
+ totalErrors: Array.from(this._errorCount.values()).reduce((a, b) => a + b, 0),
322
+ };
323
+ }
324
+ }
325
+
326
+ // Singleton instance
327
+ const registry = new ToolRegistry();
328
+
329
+ module.exports = { ToolRegistry, registry, MODULE_TYPES, TOOL_INTERFACE };
@@ -0,0 +1,60 @@
1
+ # DevAssist GitHub Actions CI 模板
2
+ # 复制到 .github/workflows/devassist.yml 使用
3
+
4
+ name: DevAssist Check
5
+
6
+ on:
7
+ push:
8
+ branches: [main, develop]
9
+ pull_request:
10
+ branches: [main, develop]
11
+
12
+ jobs:
13
+ devassist:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0 # 完整历史,用于 diff 检查
19
+
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: '20'
24
+
25
+ - name: Install DevAssist
26
+ run: npm install -g devassist
27
+
28
+ - name: Initialize DevAssist (if not already)
29
+ run: devassist init --force
30
+ continue-on-error: true
31
+
32
+ - name: Run diff check (only changed files)
33
+ run: devassist diff --base origin/${{ github.base_ref || 'main' }} --json --fail-on block
34
+ id: diff-check
35
+ continue-on-error: true
36
+
37
+ - name: Run full check
38
+ if: steps.diff-check.outcome != 'success'
39
+ run: devassist check --json
40
+
41
+ - name: Generate report
42
+ if: always()
43
+ run: devassist report
44
+
45
+ - name: Upload report artifact
46
+ if: always()
47
+ uses: actions/upload-artifact@v4
48
+ with:
49
+ name: devassist-report
50
+ path: .devassist/reports/latest.html
51
+ retention-days: 30
52
+
53
+ # 可选:AI 分析(需要 API Key)
54
+ # - name: AI Analysis
55
+ # if: always() && env.DASHSCOPE_API_KEY != ''
56
+ # env:
57
+ # DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
58
+ # run: |
59
+ # devassist ai --setup --provider qwen --key $DASHSCOPE_API_KEY
60
+ # devassist ai --analyze
@@ -0,0 +1,30 @@
1
+ # DevAssist GitLab CI 模板
2
+ # 复制到 .gitlab-ci.yml 的相应位置使用
3
+
4
+ devassist-check:
5
+ stage: test
6
+ image: node:20
7
+ before_script:
8
+ - npm install -g devassist
9
+ - devassist init --force || true
10
+ script:
11
+ # 检查变更文件
12
+ - devassist diff --staged --json --fail-on block || EXIT_CODE=$?
13
+ # 如果 diff 失败,运行完整检查
14
+ - if [ "$EXIT_CODE" != "0" ]; then devassist check --json; fi
15
+ # 生成报告
16
+ - devassist report
17
+ after_script:
18
+ - |
19
+ if [ -f .devassist/reports/latest.html ]; then
20
+ echo "DevAssist 报告已生成:.devassist/reports/latest.html"
21
+ fi
22
+ artifacts:
23
+ when: always
24
+ paths:
25
+ - .devassist/reports/
26
+ expire_in: 30 days
27
+ rules:
28
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
29
+ - if: $CI_COMMIT_BRANCH == "main"
30
+ - if: $CI_COMMIT_BRANCH == "develop"
@@ -0,0 +1,18 @@
1
+ #!/bin/sh
2
+ # DevAssist pre-commit hook 模板
3
+ # 安装方法:cp templates/ci/pre-commit-hook.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
4
+
5
+ echo "[DevAssist] 正在运行提交前检查..."
6
+
7
+ # 运行变更文件检查(只检查暂存文件,速度更快)
8
+ node "$(dirname "$0")/../../node_modules/devassist/bin/devassist.js" diff --staged --fail-on block
9
+
10
+ if [ $? -ne 0 ]; then
11
+ echo ""
12
+ echo "[DevAssist] 提交已阻断。修复上述问题后重试。"
13
+ echo "[DevAssist] 或使用 git commit --no-verify 跳过检查(不推荐)。"
14
+ exit 1
15
+ fi
16
+
17
+ echo "[DevAssist] 检查通过。"
18
+ exit 0
@@ -0,0 +1,61 @@
1
+ #!/bin/sh
2
+ # DevAssist pre-commit hook
3
+ #
4
+ # This hook runs devassist check before allowing a commit.
5
+ # It catches code quality issues, convention violations, and impact analysis
6
+ # problems BEFORE they enter your codebase.
7
+ #
8
+ # To bypass: git commit --no-verify (use sparingly!)
9
+ #
10
+ # Installation:
11
+ # Method 1: devassist init (auto-installs if .git exists)
12
+ # Method 2: cp this file to .git/hooks/pre-commit && chmod +x
13
+
14
+ set -e
15
+
16
+ # Find the devassist binary
17
+ DEVASSIST_BIN=""
18
+ candidates=(
19
+ "./node_modules/.bin/devassist"
20
+ "devassist"
21
+ "npx devassist"
22
+ )
23
+
24
+ for cmd in "${candidates[@]}"; do
25
+ if command -v "$cmd" &>/dev/null || [ -x "$cmd" ]; then
26
+ DEVASSIST_BIN="$cmd"
27
+ break
28
+ fi
29
+ done
30
+
31
+ # Fall back to direct node execution
32
+ if [ -z "$DEVASSIST_BIN" ]; then
33
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
34
+ DEVASSIST_JS="$SCRIPT_DIR/../../devassist/bin/devassist.js"
35
+ if [ -f "$DEVASSIST_JS" ]; then
36
+ DEVASSIST_BIN="node $DEVASSIST_JS"
37
+ fi
38
+ fi
39
+
40
+ if [ -z "$DEVASSIST_BIN" ]; then
41
+ echo "[DevAssist] Warning: devassist not found. Skipping pre-commit checks."
42
+ echo "[DevAssist] Install with: npm install -g devassist"
43
+ exit 0
44
+ fi
45
+
46
+ echo "[DevAssist] Running pre-commit checks..."
47
+
48
+ # Run check in pre-commit mode (faster, skips AI analysis)
49
+ $DEVASSIST_BIN check --pre-commit 2>&1
50
+ RESULT=$?
51
+
52
+ if [ $RESULT -ne 0 ]; then
53
+ echo ""
54
+ echo "[DevAssist] ❌ Commit BLOCKED — check failures detected."
55
+ echo "[DevAssist] Fix the issues above, or bypass with: git commit --no-verify"
56
+ echo ""
57
+ exit 1
58
+ fi
59
+
60
+ echo "[DevAssist] ✅ All checks passed."
61
+ exit 0