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,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
|