jsharness 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/.harness/README.md +199 -0
- package/.harness/agents/code-reviewer/contract.yaml +64 -0
- package/.harness/agents/developer/contract.yaml +72 -0
- package/.harness/agents/gate-controller/contract.yaml +64 -0
- package/.harness/agents/project-manager/contract.yaml +77 -0
- package/.harness/agents/prompt-templates.md +352 -0
- package/.harness/agents/requirements-analyst/contract.yaml +64 -0
- package/.harness/agents/solution-designer/contract.yaml +75 -0
- package/.harness/agents/tester/contract.yaml +92 -0
- package/.harness/config/models.yaml +67 -0
- package/.harness/dev-map/backend/api-definition.md +131 -0
- package/.harness/dev-map/backend/auth-security.md +131 -0
- package/.harness/dev-map/backend/conventions-java.md +471 -0
- package/.harness/dev-map/backend/conventions.md +192 -0
- package/.harness/dev-map/backend/database.md +106 -0
- package/.harness/dev-map/backend/structure.md +140 -0
- package/.harness/dev-map/decisions.md +275 -0
- package/.harness/dev-map/frontend/api-integration.md +139 -0
- package/.harness/dev-map/frontend/components.md +178 -0
- package/.harness/dev-map/frontend/conventions.md +416 -0
- package/.harness/dev-map/frontend/state-management.md +170 -0
- package/.harness/dev-map/frontend/structure.md +103 -0
- package/.harness/dev-map/overview.md +267 -0
- package/.harness/docs/integration-test-plan.md +248 -0
- package/.harness/docs/team-guidelines/README.md +161 -0
- package/.harness/docs/team-guidelines/arch-team.md +811 -0
- package/.harness/docs/team-guidelines/collaboration.md +556 -0
- package/.harness/docs/team-guidelines/pm-team.md +337 -0
- package/.harness/docs/team-guidelines/qa-team.md +562 -0
- package/.harness/docs/team-guidelines/rd-team.md +714 -0
- package/.harness/docs/training-materials.md +280 -0
- package/.harness/gate/baseline.js +220 -0
- package/.harness/gate/checks/build-gates-frontend.js +152 -0
- package/.harness/gate/checks/build-gates-java.js +155 -0
- package/.harness/gate/checks/build-gates.js +119 -0
- package/.harness/gate/checks/engineering-consistency.js +138 -0
- package/.harness/gate/checks/security-quality.js +129 -0
- package/.harness/gate/checks/static-compliance.js +313 -0
- package/.harness/gate/checks/test-compliance.js +114 -0
- package/.harness/gate/index.js +315 -0
- package/.harness/mcp/config.yaml +435 -0
- package/.harness/rules/global/coding-standard.md +232 -0
- package/.harness/rules/global/commit-convention.md +165 -0
- package/.harness/rules/global/process-discipline.md +192 -0
- package/.harness/rules/global/security-baseline.md +306 -0
- package/.harness/rules/project/frontend-vue3.md +293 -0
- package/.harness/rules/project/java-backend.md +460 -0
- package/.harness/rules/project/web-specific.md +231 -0
- package/.harness/skills/build.md +192 -0
- package/.harness/skills/code-review.md +251 -0
- package/.harness/skills/docker-build.md +227 -0
- package/.harness/skills/docs-update.md +164 -0
- package/.harness/skills/java-build.md +261 -0
- package/.harness/skills/lint-check.md +482 -0
- package/.harness/skills/task-board-maintenance.md +105 -0
- package/.harness/skills/test-api.md +461 -0
- package/.harness/skills/test-e2e.md +431 -0
- package/.harness/skills/test-unit.md +649 -0
- package/.harness/skills/vue-frontend-build.md +344 -0
- package/.harness/specs/quality-feedback/implementation-guide.md +350 -0
- package/.harness/task-board.md +121 -0
- package/.harness/workflow/definition.yaml +504 -0
- package/.harness/workflow/validate.js +320 -0
- package/.harness/workflow/variants.yaml +253 -0
- package/README.md +237 -0
- package/bin/jsharness.js +53 -0
- package/lib/index.mjs +778 -0
- package/package.json +1 -0
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Engineering - Core Library
|
|
3
|
+
*
|
|
4
|
+
* 多适配器架构:检测 AI 工具 → 转换规则格式 → 注入到对应位置
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// 支持的 AI 工具清单
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
export const SUPPORTED_TOOLS = [
|
|
18
|
+
{
|
|
19
|
+
id: 'codebuddy',
|
|
20
|
+
name: 'CodeBuddy (腾讯云代码助手)',
|
|
21
|
+
description: '.codebuddy/rules/ + .codebuddy/skills/',
|
|
22
|
+
detector: hasDir('.codebuddy') || hasFile('CODEBUDDY.md'),
|
|
23
|
+
priority: 100,
|
|
24
|
+
ruleFormat: 'markdown',
|
|
25
|
+
skillFormat: 'markdown',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'cursor',
|
|
29
|
+
name: 'Cursor',
|
|
30
|
+
description: '.cursorrules 文件',
|
|
31
|
+
detector: hasFile('.cursorrules'),
|
|
32
|
+
priority: 90,
|
|
33
|
+
ruleFormat: 'cursor-md',
|
|
34
|
+
skillFormat: null, // Cursor 不支持独立 skills
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'copilot',
|
|
38
|
+
name: 'GitHub Copilot',
|
|
39
|
+
description: '.github/copilot-instructions.md',
|
|
40
|
+
detector: hasDir('.github'),
|
|
41
|
+
priority: 80,
|
|
42
|
+
ruleFormat: 'copilot-md',
|
|
43
|
+
skillFormat: null,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'windsurf',
|
|
47
|
+
name: 'Windsurf (Codeium)',
|
|
48
|
+
description: '.windsurfrules 目录',
|
|
49
|
+
detector: hasFile('.windsurfrules') || hasDir('.windsurf'),
|
|
50
|
+
priority: 70,
|
|
51
|
+
ruleFormat: 'windsurf-md',
|
|
52
|
+
skillFormat: null,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'continue',
|
|
56
|
+
name: 'Continue (VS Code)',
|
|
57
|
+
description: 'continue.config.json',
|
|
58
|
+
detector: hasFile('continue.config.json'),
|
|
59
|
+
priority: 60,
|
|
60
|
+
ruleFormat: 'continue-json',
|
|
61
|
+
skillFormat: null,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'cline',
|
|
65
|
+
name: 'Cline (VS Code)',
|
|
66
|
+
description: '.clinerules 文件',
|
|
67
|
+
detector: hasFile('.clinerules'),
|
|
68
|
+
priority: 50,
|
|
69
|
+
ruleFormat: 'cline-md',
|
|
70
|
+
skillFormat: null,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'qoder',
|
|
74
|
+
name: 'Qoder (阿里云)',
|
|
75
|
+
description: '.qoder/rules/ 目录',
|
|
76
|
+
detector: hasDir('.qoder'),
|
|
77
|
+
priority: 95,
|
|
78
|
+
ruleFormat: 'markdown',
|
|
79
|
+
skillFormat: 'markdown',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'codex',
|
|
83
|
+
name: 'Codex CLI (OpenAI)',
|
|
84
|
+
description: 'AGENTS.md 文件(项目根目录)',
|
|
85
|
+
detector: hasFile('AGENTS.md') || hasDir('.codex'),
|
|
86
|
+
priority: 85,
|
|
87
|
+
ruleFormat: 'agents-md',
|
|
88
|
+
skillFormat: null,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'claude-code',
|
|
92
|
+
name: 'Claude Code (Anthropic)',
|
|
93
|
+
description: '.claude/rules/ + CLAUDE.md',
|
|
94
|
+
detector: hasFile('CLAUDE.md') || hasDir('.claude'),
|
|
95
|
+
priority: 93,
|
|
96
|
+
ruleFormat: 'claude-rules-md',
|
|
97
|
+
skillFormat: 'claude-skills-md',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'trae',
|
|
101
|
+
name: 'Trae (字节跳动)',
|
|
102
|
+
description: '.trae/rules/project_rules.md',
|
|
103
|
+
detector: hasDir('.trae') || hasFile('.trae/rules/project_rules.md'),
|
|
104
|
+
priority: 75,
|
|
105
|
+
ruleFormat: 'trae-rules-md',
|
|
106
|
+
skillFormat: null,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// ============================================================
|
|
111
|
+
// 检测工具函数
|
|
112
|
+
// ============================================================
|
|
113
|
+
|
|
114
|
+
function hasDir(name) {
|
|
115
|
+
return () => fs.existsSync(name) && fs.lstatSync(name).isDirectory();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasFile(name) {
|
|
119
|
+
return () => fs.existsSync(name) && fs.lstatSync(name).isFile();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 自动检测当前项目使用的 AI 工具
|
|
124
|
+
*/
|
|
125
|
+
export function detectTool(projectDir) {
|
|
126
|
+
const results = [];
|
|
127
|
+
|
|
128
|
+
for (const tool of SUPPORTED_TOOLS) {
|
|
129
|
+
try {
|
|
130
|
+
const detectFn = tool.detector;
|
|
131
|
+
const isDetected = typeof detectFn === 'function'
|
|
132
|
+
? detectFn()
|
|
133
|
+
: fs.existsSync(path.join(projectDir, typeof detectFn === 'string' ? detectFn : ''));
|
|
134
|
+
|
|
135
|
+
if (isDetected) {
|
|
136
|
+
results.push({ ...tool, detected: true });
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// 检测失败,跳过
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 按优先级排序,返回最匹配的
|
|
144
|
+
results.sort((a, b) => b.priority - a.priority);
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================
|
|
149
|
+
// 规则转换引擎
|
|
150
|
+
// ============================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 将 .harness/rules/*.md 转换为目标工具的格式
|
|
154
|
+
*/
|
|
155
|
+
export function transformRules(ruleFiles, targetToolId, options = {}) {
|
|
156
|
+
const transformer = RULE_TRANSFORMERS[targetToolId] || RULE_TRANSFORMERS['generic'];
|
|
157
|
+
return transformer(ruleFiles, options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 将 .harness/skills/*.md 转换为目标工具的格式
|
|
162
|
+
*/
|
|
163
|
+
export function transformSkills(skillFiles, targetToolId, options = {}) {
|
|
164
|
+
const transformer = SKILL_TRANSFORMERS[targetToolId] || SKILL_TRANSFORMERS['generic'];
|
|
165
|
+
return transformer(skillFiles, options);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================
|
|
169
|
+
// 各工具格式的转换器
|
|
170
|
+
// ============================================================
|
|
171
|
+
|
|
172
|
+
const RULE_TRANSFORMERS = {
|
|
173
|
+
|
|
174
|
+
/** CodeBuddy: 原样复制 .md 到 .codebuddy/rules/ */
|
|
175
|
+
codebuddy(ruleFiles, opts) {
|
|
176
|
+
const outputs = [];
|
|
177
|
+
for (const file of ruleFiles) {
|
|
178
|
+
const content = fs.readFileSync(file.path, 'utf-8');
|
|
179
|
+
outputs.push({
|
|
180
|
+
relativePath: `.codebuddy/rules/${file.name}`,
|
|
181
|
+
content,
|
|
182
|
+
source: file.path,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return { format: 'directory', files: outputs };
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** Cursor: 合并所有规则为 .cursorrules */
|
|
189
|
+
cursor(ruleFiles, opts) {
|
|
190
|
+
let content = `# Harness Engineering Rules\n`;
|
|
191
|
+
content += `> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
192
|
+
|
|
193
|
+
for (const file of ruleFiles) {
|
|
194
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
195
|
+
content += extractBody(raw);
|
|
196
|
+
content += '\n---\n\n';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { format: 'single-file', files: [{ relativePath: '.cursorrules', content }] };
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/** GitHub Copilot: 合并为 .github/copilot-instructions.md */
|
|
203
|
+
copilot(ruleFiles, opts) {
|
|
204
|
+
let content = `# Project Rules & Guidelines\n\n`;
|
|
205
|
+
content += `## Harness Engineering System\n\n`;
|
|
206
|
+
|
|
207
|
+
for (const file of ruleFiles) {
|
|
208
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
209
|
+
content += `### ${extractTitle(raw)}\n\n`;
|
|
210
|
+
content += extractBody(raw);
|
|
211
|
+
content += '\n\n';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { format: 'single-file', files: [{ relativePath: '.github/copilot-instructions.md', content }] };
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/** Windsurf: .windsurfrules 格式 */
|
|
218
|
+
windsurf(ruleFiles, opts) {
|
|
219
|
+
let content = '';
|
|
220
|
+
for (const file of ruleFiles) {
|
|
221
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
222
|
+
content += `<rules>\n`;
|
|
223
|
+
content += `<rule name="${file.name.replace('.md', '')}">\n`;
|
|
224
|
+
content += extractBody(raw).trim();
|
|
225
|
+
content += `\n</rule>\n</rules>\n\n`;
|
|
226
|
+
}
|
|
227
|
+
return { format: 'single-file', files: [{ relativePath: '.windsurfrules', content }] };
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/** Continue: continue.config.json 格式 */
|
|
231
|
+
continue(ruleFiles, opts) {
|
|
232
|
+
const rules = [];
|
|
233
|
+
for (const file of ruleFiles) {
|
|
234
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
235
|
+
rules.push({
|
|
236
|
+
name: file.name.replace('.md', ''),
|
|
237
|
+
type: 'rule',
|
|
238
|
+
description: extractDescription(raw),
|
|
239
|
+
content: extractBody(raw),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const config = { rules };
|
|
243
|
+
const content = JSON.stringify(config, null, 2);
|
|
244
|
+
return { format: 'json-config', files: [{ relativePath: 'continue.config.json', content }] };
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/** Cline: .clinerules 格式(类似 Cursor) */
|
|
248
|
+
cline(ruleFiles, opts) {
|
|
249
|
+
let content = `# Harness Engineering Rules for Cline\n\n`;
|
|
250
|
+
for (const file of ruleFiles) {
|
|
251
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
252
|
+
content += `## ${extractTitle(raw)}\n\n`;
|
|
253
|
+
content += extractBody(raw);
|
|
254
|
+
content += '\n---\n\n';
|
|
255
|
+
}
|
|
256
|
+
return { format: 'single-file', files: [{ relativePath: '.clinerules', content }] };
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* ================================================================
|
|
261
|
+
* 新增工具适配器 (v1.1+)
|
|
262
|
+
* ================================================================
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
/** Qoder: 原样复制 .md 到 .qoder/rules/ (与 CodeBuddy 相同模式) */
|
|
266
|
+
qoder(ruleFiles, opts) {
|
|
267
|
+
const outputs = [];
|
|
268
|
+
for (const file of ruleFiles) {
|
|
269
|
+
const content = fs.readFileSync(file.path, 'utf-8');
|
|
270
|
+
outputs.push({
|
|
271
|
+
relativePath: `.qoder/rules/${file.name}`,
|
|
272
|
+
content,
|
|
273
|
+
source: file.path,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return { format: 'directory', files: outputs };
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/** Codex CLI: 合并所有规则为 AGENTS.md(项目根目录) */
|
|
280
|
+
codex(ruleFiles, opts) {
|
|
281
|
+
let content = `# Harness Engineering - Project Instructions for Codex CLI\n`;
|
|
282
|
+
content += `> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n`;
|
|
283
|
+
content += `> Source: .harness/ rules & skills system\n\n`;
|
|
284
|
+
|
|
285
|
+
// Codex AGENTS.md 推荐结构:Commands / Architecture / Conventions / Important
|
|
286
|
+
content += `## Overview\n\n`;
|
|
287
|
+
content += `This project uses **Harness Engineering** system with the following tech stack:\n\n`;
|
|
288
|
+
content += `- Frontend: Vue3 + Vite + TypeScript + Pinia + Element Plus\n`;
|
|
289
|
+
content += `- Backend: Spring Boot 3.2 + JDK21 + MyBatis-Plus + Nacos\n\n`;
|
|
290
|
+
|
|
291
|
+
content += `---\n\n## Conventions & Rules\n\n`;
|
|
292
|
+
|
|
293
|
+
for (const file of ruleFiles) {
|
|
294
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
295
|
+
content += `### ${extractTitle(raw)}\n\n`;
|
|
296
|
+
content += extractBody(raw);
|
|
297
|
+
content += '\n\n';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 追加 Skills 摘要
|
|
301
|
+
content += `---\n\n## Available Build/Test Skills\n\n`;
|
|
302
|
+
content += 'See .harness/skills/ directory for detailed build, lint, test, and review procedures.\n';
|
|
303
|
+
content += `- \`npm run build\` — Vue3 frontend production build\n`;
|
|
304
|
+
content += `- \`mvn compile test\` — Java backend build with tests\n`;
|
|
305
|
+
content += `- \`node .harness/gate/index.js\` — Run gate compliance checks\n\n`;
|
|
306
|
+
|
|
307
|
+
return { format: 'single-file', files: [{ relativePath: 'AGENTS.md', content }] };
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/** Claude Code: 输出为 .claude/rules/*.md(支持 paths: frontmatter 路径限定) */
|
|
311
|
+
'claude-code'(ruleFiles, opts) {
|
|
312
|
+
const outputs = [];
|
|
313
|
+
|
|
314
|
+
// 生成 CLAUDE.md 主文件(精简版,<200行)
|
|
315
|
+
let claudeMd = `# Harness Engineering System\n\n`;
|
|
316
|
+
claudeMd += `> Auto-generated by hariness. Full rules in \`.claude/rules/\`\n\n`;
|
|
317
|
+
claudeMd += `## Tech Stack\n`;
|
|
318
|
+
claudeMd += `- **Frontend**: Vue3 + Vite + TypeScript + Pinia + Element Plus\n`;
|
|
319
|
+
claudeMd += `- **Backend**: Spring Boot 3.2 (JDK21 Virtual Threads) + MyBatis-Plus\n\n`;
|
|
320
|
+
claudeMd += `## Commands\n`;
|
|
321
|
+
claudeMd += `\`\`\`bash\n`;
|
|
322
|
+
claudeMd += `npm run build # Frontend build\n`;
|
|
323
|
+
claudeMd += `npm run lint # ESLint check\n`;
|
|
324
|
+
claudeMd += `mvn compile test # Backend build + test\n`;
|
|
325
|
+
claudeMd += `node .harness/gate/index.js # Gate checks\n`;
|
|
326
|
+
claudeMd += `\`\`\`\n\n`;
|
|
327
|
+
claudeMd += `> Detailed rules are loaded from \`.claude/rules/\` files below.\n`;
|
|
328
|
+
|
|
329
|
+
outputs.push({ relativePath: 'CLAUDE.md', content: claudeMd });
|
|
330
|
+
|
|
331
|
+
// 为每个规则文件生成独立的 .claude/rules/*.md,带路径限定 frontmatter
|
|
332
|
+
for (const file of ruleFiles) {
|
|
333
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
334
|
+
const body = extractBody(raw);
|
|
335
|
+
|
|
336
|
+
// 根据规则类别决定 paths 范围
|
|
337
|
+
let scopePaths = null;
|
|
338
|
+
if (file.category === 'project') {
|
|
339
|
+
if (file.relativePath.includes('vue3') || file.relativePath.includes('frontend') || file.relativePath.includes('web')) {
|
|
340
|
+
scopePaths = ['src/**/*.{ts,vue,js}', '**/*.{ts,vue,js}'];
|
|
341
|
+
} else if (file.relativePath.includes('java') || file.relativePath.includes('backend')) {
|
|
342
|
+
scopePaths = ['src/main/**/*.java', '**/*.java'];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let content = '';
|
|
347
|
+
if (scopePaths) {
|
|
348
|
+
content += `---\npaths:\n`;
|
|
349
|
+
for (const p of scopePaths) {
|
|
350
|
+
content += ` - "${p}"\n`;
|
|
351
|
+
}
|
|
352
|
+
content += `---\n\n`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
content += body;
|
|
356
|
+
|
|
357
|
+
outputs.push({
|
|
358
|
+
relativePath: `.claude/rules/${file.name}`,
|
|
359
|
+
content,
|
|
360
|
+
source: file.path,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { format: 'multi-file', files: outputs };
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
/** Trae: 合并为 .trae/rules/project_rules.md */
|
|
368
|
+
trae(ruleFiles, opts) {
|
|
369
|
+
let content = `# Harness Engineering Rules for Trae\n\n`;
|
|
370
|
+
content += `> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
371
|
+
content += `## 技术栈\n\n`;
|
|
372
|
+
content += `- 前端: Vue3 + Vite + TypeScript + Pinia + Element Plus\n`;
|
|
373
|
+
content += `- 后端: Spring Boot 3.2 + JDK21 + MyBatis-Plus\n\n`;
|
|
374
|
+
content += `---\n\n`;
|
|
375
|
+
|
|
376
|
+
for (const file of ruleFiles) {
|
|
377
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
378
|
+
content += `### ${extractTitle(raw)}\n\n`;
|
|
379
|
+
content += extractBody(raw);
|
|
380
|
+
content += '\n\n---\n\n';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { format: 'single-file', files: [{ relativePath: '.trae/rules/project_rules.md', content }] };
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/** Generic fallback */
|
|
387
|
+
generic(ruleFiles, opts) {
|
|
388
|
+
return RULE_TRANSFORMERS.codebuddy(ruleFiles, opts);
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const SKILL_TRANSFORMERS = {
|
|
393
|
+
|
|
394
|
+
/** CodeBuddy: 复制到 .codebuddy/skills/ */
|
|
395
|
+
codebuddy(skillFiles, opts) {
|
|
396
|
+
const outputs = [];
|
|
397
|
+
for (const file of skillFiles) {
|
|
398
|
+
const content = fs.readFileSync(file.path, 'utf-8');
|
|
399
|
+
outputs.push({
|
|
400
|
+
relativePath: `.codebuddy/skills/${file.name}`,
|
|
401
|
+
content,
|
|
402
|
+
source: file.path,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return { format: 'directory', files: outputs };
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/** 不支持 skills 的工具 → 合并到规则文件中 */
|
|
409
|
+
generic(skillFiles, opts) {
|
|
410
|
+
const outputs = [];
|
|
411
|
+
for (const file of skillFiles) {
|
|
412
|
+
const content = fs.readFileSync(file.path, 'utf-8');
|
|
413
|
+
outputs.push({
|
|
414
|
+
relativePath: `.harness/skills/${file.name}`, // 保持原始位置作为参考
|
|
415
|
+
content,
|
|
416
|
+
source: file.path,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return { format: 'reference', files: outputs };
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/** Qoder: 复制到 .qoder/skills/ */
|
|
423
|
+
qoder(skillFiles, opts) {
|
|
424
|
+
const outputs = [];
|
|
425
|
+
for (const file of skillFiles) {
|
|
426
|
+
const content = fs.readFileSync(file.path, 'utf-8');
|
|
427
|
+
outputs.push({
|
|
428
|
+
relativePath: `.qoder/skills/${file.name}`,
|
|
429
|
+
content,
|
|
430
|
+
source: file.path,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return { format: 'directory', files: outputs };
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
/** Claude Code: 复制到 .claude/skills/ (Claude 支持自动触发的 skills) */
|
|
437
|
+
'claude-code'(skillFiles, opts) {
|
|
438
|
+
const outputs = [];
|
|
439
|
+
for (const file of skillFiles) {
|
|
440
|
+
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
441
|
+
const body = extractBody(raw);
|
|
442
|
+
|
|
443
|
+
// Claude Code skills 格式:YAML frontmatter + Markdown body
|
|
444
|
+
let content = `---\n`;
|
|
445
|
+
content += `name: ${file.name.replace('.md', '')}\n`;
|
|
446
|
+
content += `description: Harness Engineering Skill - ${extractDescription(raw) || file.name}\n`;
|
|
447
|
+
// 自动触发条件
|
|
448
|
+
const lowerName = file.name.toLowerCase();
|
|
449
|
+
if (lowerName.includes('build')) content += `trigger_type: manual\n`;
|
|
450
|
+
else if (lowerName.includes('test')) content += `trigger_type: on_save\n`;
|
|
451
|
+
else if (lowerName.includes('lint') || lowerName.includes('review')) content += `trigger_type: on_pr\n`;
|
|
452
|
+
else content += `trigger_type: manual\n`;
|
|
453
|
+
content += `---\n\n`;
|
|
454
|
+
|
|
455
|
+
content += body;
|
|
456
|
+
|
|
457
|
+
outputs.push({
|
|
458
|
+
relativePath: `.claude/skills/${file.name}`,
|
|
459
|
+
content,
|
|
460
|
+
source: file.path,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return { format: 'directory', files: outputs };
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// ============================================================
|
|
468
|
+
// Markdown 解析辅助函数
|
|
469
|
+
// ============================================================
|
|
470
|
+
|
|
471
|
+
function extractTitle(markdown) {
|
|
472
|
+
const match = markdown.match(/^#\s+(.+)/m);
|
|
473
|
+
return match ? match[1].trim() : 'Untitled Rule';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function extractDescription(markdown) {
|
|
477
|
+
// 尝试提取第一段描述性文字
|
|
478
|
+
const body = markdown.replace(/^#.+/gm, '').trim();
|
|
479
|
+
const firstPara = body.match(/^(.+)/m);
|
|
480
|
+
return firstPara ? firstPara[1].slice(0, 120) : '';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function extractBody(markdown) {
|
|
484
|
+
// 移除 frontmatter (--- ... ---)
|
|
485
|
+
let body = markdown;
|
|
486
|
+
if (body.startsWith('---')) {
|
|
487
|
+
const endIdx = body.indexOf('---', 3);
|
|
488
|
+
if (endIdx > 0) {
|
|
489
|
+
body = body.slice(endIdx + 3).trim();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return body;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================
|
|
496
|
+
// 扫描 Harness 源文件
|
|
497
|
+
// ============================================================
|
|
498
|
+
|
|
499
|
+
export function scanHarnessRules(harnessDir, stackFilter) {
|
|
500
|
+
const rulesDir = path.join(harnessDir, 'rules');
|
|
501
|
+
const results = [];
|
|
502
|
+
|
|
503
|
+
function scanDir(dir, category) {
|
|
504
|
+
if (!fs.existsSync(dir)) return;
|
|
505
|
+
const entries = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const filePath = path.join(dir, entry);
|
|
508
|
+
const relPath = path.relative(harnessDir, filePath);
|
|
509
|
+
|
|
510
|
+
// 技术栈过滤
|
|
511
|
+
if (stackFilter && stackFilter !== 'all') {
|
|
512
|
+
const lowerRel = relPath.toLowerCase();
|
|
513
|
+
if (stackFilter === 'vue3' && !lowerRel.includes('vue') && !lowerRel.includes('frontend') && !lowerRel.includes('global')) {
|
|
514
|
+
// 对于 vue3 过滤,保留 global 和 frontend/vue 相关
|
|
515
|
+
if (!lowerRel.includes('global') && !lowerRel.includes('frontend') && !lowerRel.includes('web')) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (stackFilter === 'java' && !lowerRel.includes('java') && !lowerRel.includes('backend') && !lowerRel.includes('global')) {
|
|
520
|
+
if (!lowerRel.includes('global') && !lowerRel.includes('backend')) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
results.push({
|
|
527
|
+
name: entry,
|
|
528
|
+
path: filePath,
|
|
529
|
+
category,
|
|
530
|
+
relativePath: relPath,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
scanDir(path.join(rulesDir, 'global'), 'global');
|
|
536
|
+
scanDir(path.join(rulesDir, 'project'), 'project');
|
|
537
|
+
|
|
538
|
+
return results;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function scanHarnessSkills(harnessDir, stackFilter) {
|
|
542
|
+
const skillsDir = path.join(harnessDir, 'skills');
|
|
543
|
+
const results = [];
|
|
544
|
+
|
|
545
|
+
if (!fs.existsSync(skillsDir)) return results;
|
|
546
|
+
|
|
547
|
+
const entries = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md'));
|
|
548
|
+
for (const entry of entries) {
|
|
549
|
+
const filePath = path.join(skillsDir, entry);
|
|
550
|
+
results.push({
|
|
551
|
+
name: entry,
|
|
552
|
+
path: filePath,
|
|
553
|
+
category: 'skill',
|
|
554
|
+
relativePath: path.relative(harnessDir, filePath),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return results;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ============================================================
|
|
562
|
+
// 写入目标位置
|
|
563
|
+
// ============================================================
|
|
564
|
+
|
|
565
|
+
export function injectOutputs(projectDir, outputs, options = {}) {
|
|
566
|
+
const { force = false, verbose = false } = options;
|
|
567
|
+
const written = [];
|
|
568
|
+
const skipped = [];
|
|
569
|
+
|
|
570
|
+
for (const output of outputs) {
|
|
571
|
+
const targetPath = path.join(projectDir, output.relativePath);
|
|
572
|
+
const targetDir = path.dirname(targetPath);
|
|
573
|
+
|
|
574
|
+
// 创建目录
|
|
575
|
+
if (!fs.existsSync(targetDir)) {
|
|
576
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 检查是否已存在
|
|
580
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
581
|
+
skipped.push(output.relativePath);
|
|
582
|
+
if (verbose) console.log(` ⏭ 跳过 (已存在): ${output.relativePath}`);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
fs.writeFileSync(targetPath, output.content, 'utf-8');
|
|
587
|
+
written.push(output.relativePath);
|
|
588
|
+
if (verbose) console.log(` ✅ 已写入: ${output.relativePath}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { written, skipped };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ============================================================
|
|
595
|
+
// CLI 命令实现
|
|
596
|
+
// ============================================================
|
|
597
|
+
|
|
598
|
+
export async function runInit(projectDir, options = {}) {
|
|
599
|
+
const {
|
|
600
|
+
tool: requestedTool,
|
|
601
|
+
stack = 'all',
|
|
602
|
+
rulesOnly = false,
|
|
603
|
+
skillsOnly = false,
|
|
604
|
+
force = false,
|
|
605
|
+
verbose = false,
|
|
606
|
+
} = options;
|
|
607
|
+
|
|
608
|
+
console.log('\n🔧 Harness Engineering 初始化');
|
|
609
|
+
console.log(` 目标目录: ${projectDir}\n`);
|
|
610
|
+
|
|
611
|
+
// 1. 确定 Harness 源路径
|
|
612
|
+
const harnessDir = findHarnessSource(projectDir);
|
|
613
|
+
if (!harnessDir) {
|
|
614
|
+
console.error('❌ 未找到 .harness/ 目录。请确保在包含 .harness/ 的项目或包中运行。');
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
if (verbose) console.log(` 📂 Harness 源: ${harnessDir}`);
|
|
618
|
+
|
|
619
|
+
// 2. 检测或指定 AI 工具
|
|
620
|
+
let targetTools = [];
|
|
621
|
+
if (requestedTool) {
|
|
622
|
+
const found = SUPPORTED_TOOLS.find(t => t.id === requestedTool || t.name.includes(requestedTool));
|
|
623
|
+
if (found) {
|
|
624
|
+
targetTools = [{ ...found, detected: true }];
|
|
625
|
+
} else {
|
|
626
|
+
console.error(`❌ 不支持的工具: ${requestedTool}`);
|
|
627
|
+
console.log(` 可用工具: ${SUPPORTED_TOOLS.map(t => t.id).join(', ')}`);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
targetTools = detectTool(projectDir);
|
|
632
|
+
if (targetTools.length === 0) {
|
|
633
|
+
console.log('⚠️ 未自动检测到 AI 工具。将使用默认模式(CodeBuddy 格式)。\n');
|
|
634
|
+
console.log(' 提示: 使用 --tool <name> 指定工具,或运行 npx hariness list-tools 查看');
|
|
635
|
+
targetTools = [{ ...SUPPORTED_TOOLS[0], detected: true }]; // 默认 CodeBuddy
|
|
636
|
+
} else {
|
|
637
|
+
console.log(`🔍 检测到的 AI 工具:`);
|
|
638
|
+
for (const t of targetTools) {
|
|
639
|
+
console.log(` • ${t.name} (${t.description})`);
|
|
640
|
+
}
|
|
641
|
+
console.log('');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 3. 扫描源文件
|
|
646
|
+
const allRuleFiles = scanHarnessRules(harnessDir, stack);
|
|
647
|
+
const allSkillFiles = scanHarnessSkills(harnessDir, stack);
|
|
648
|
+
|
|
649
|
+
if (verbose) {
|
|
650
|
+
console.log(`📋 扫描结果: ${allRuleFiles.length} 个规则, ${allSkillFiles.length} 个技能\n`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 4. 对每个目标工具执行注入
|
|
654
|
+
const summary = [];
|
|
655
|
+
|
|
656
|
+
for (const tool of targetTools) {
|
|
657
|
+
console.log(`\n━━━ 注入到 ${tool.name} ━━━`);
|
|
658
|
+
|
|
659
|
+
const outputs = [];
|
|
660
|
+
|
|
661
|
+
// 注入 Rules
|
|
662
|
+
if (!skillsOnly && allRuleFiles.length > 0) {
|
|
663
|
+
console.log(`\n📜 注入规则 (${allRuleFiles.length} 个)...`);
|
|
664
|
+
const result = transformRules(allRuleFiles, tool.id, { stack });
|
|
665
|
+
outputs.push(...result.files);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 注入 Skills
|
|
669
|
+
if (!rulesOnly && allSkillFiles.length > 0) {
|
|
670
|
+
console.log(`⚡ 注入技能 (${allSkillFiles.length} 个)...`);
|
|
671
|
+
const result = transformSkills(allSkillFiles, tool.id, { stack });
|
|
672
|
+
outputs.push(...result.files);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 写入文件
|
|
676
|
+
const { written, skipped } = injectOutputs(projectDir, outputs, { force, verbose });
|
|
677
|
+
|
|
678
|
+
summary.push({ tool: tool.name, written, skipped });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 5. 输出总结
|
|
682
|
+
console.log('\n═════════════════════════════');
|
|
683
|
+
console.log('✅ 初始化完成!');
|
|
684
|
+
|
|
685
|
+
for (const s of summary) {
|
|
686
|
+
if (s.written.length > 0) {
|
|
687
|
+
console.log(`\n [${s.tool}]`);
|
|
688
|
+
console.log(` ✅ 写入 ${s.written.length} 个文件:`);
|
|
689
|
+
s.written.forEach(f => console.log(` - ${f}`));
|
|
690
|
+
}
|
|
691
|
+
if (s.skipped.length > 0) {
|
|
692
|
+
console.log(` ⏭ 跳过 ${s.skipped.length} 个文件 (已存在):`);
|
|
693
|
+
s.skipped.forEach(f => console.log(` - ${f}`));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!force && summary.some(s => s.skipped.length > 0)) {
|
|
698
|
+
console.log('\n💡 提示: 使用 --force 可覆盖已有配置');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
console.log('\n🎉 Harness 规则已注入到你的 AI 工具中!开始对话即可生效。\n');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function listTools() {
|
|
705
|
+
console.log('\n支持的 AI 编程工具:\n');
|
|
706
|
+
|
|
707
|
+
for (const tool of SUPPORTED_TOOLS) {
|
|
708
|
+
const status = tool.detector ? '✅' : '○ ';
|
|
709
|
+
console.log(`${status} ${tool.id.padEnd(14)} ${tool.name}`);
|
|
710
|
+
console.log(` 格式: ${tool.ruleFormat}${tool.skillFormat ? ' + ' + tool.skillFormat : ''}`);
|
|
711
|
+
console.log(` 说明: ${tool.description}`);
|
|
712
|
+
console.log('');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
console.log('使用方法:');
|
|
716
|
+
console.log(' npx hariness init --tool <id>');
|
|
717
|
+
console.log('');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function showStatus(projectDir) {
|
|
721
|
+
console.log('\n📊 Harness 项目状态\n');
|
|
722
|
+
|
|
723
|
+
// 检查 .harness 是否存在
|
|
724
|
+
const harnessDir = path.join(projectDir, '.harness');
|
|
725
|
+
const hasHarness = fs.existsSync(harnessDir);
|
|
726
|
+
console.log(` Harness 源: ${hasHarness ? '✅ 存在' : '❌ 缺失'} (.harness/)`);
|
|
727
|
+
|
|
728
|
+
if (hasHarness) {
|
|
729
|
+
const rulesCount = scanHarnessRules(harnessDir, 'all').length;
|
|
730
|
+
const skillsCount = scanHarnessSkills(harnessDir, 'all').length;
|
|
731
|
+
console.log(` 规则数量: ${rulesCount}`);
|
|
732
|
+
console.log(` 技能数量: ${skillsCount}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// 检查各工具状态
|
|
736
|
+
const detected = detectTool(projectDir);
|
|
737
|
+
console.log(` AI 工具: ${detected.length > 0 ? detected.map(t => t.name).join(', ') : '(未检测到)'}`);
|
|
738
|
+
|
|
739
|
+
// 检查已注入的目标文件
|
|
740
|
+
const targets = [
|
|
741
|
+
{ name: 'CodeBuddy', path: '.codebuddy/rules/' },
|
|
742
|
+
{ name: 'Qoder', path: '.qoder/rules/' },
|
|
743
|
+
{ name: 'Cursor', path: '.cursorrules' },
|
|
744
|
+
{ name: 'Copilot', path: '.github/copilot-instructions.md' },
|
|
745
|
+
{ name: 'Windsurf', path: '.windsurfrules' },
|
|
746
|
+
{ name: 'Continue', path: 'continue.config.json' },
|
|
747
|
+
{ name: 'Cline', path: '.clinerules' },
|
|
748
|
+
{ name: 'Codex CLI', path: 'AGENTS.md' },
|
|
749
|
+
{ name: 'Claude Code', path: '.claude/rules/' },
|
|
750
|
+
{ name: 'Trae', path: '.trae/rules/project_rules.md' },
|
|
751
|
+
];
|
|
752
|
+
|
|
753
|
+
console.log('\n 注入状态:');
|
|
754
|
+
for (const t of targets) {
|
|
755
|
+
const fullPath = path.join(projectDir, t.path);
|
|
756
|
+
const exists = fs.existsSync(fullPath);
|
|
757
|
+
console.log(` ${exists ? '✅' : '○ '} ${t.name.padEnd(12)} ${t.path}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
console.log('');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ============================================================
|
|
764
|
+
// 内部:查找 Harness 源
|
|
765
|
+
// ============================================================
|
|
766
|
+
|
|
767
|
+
function findHarnessSource(projectDir) {
|
|
768
|
+
// 1. 项目自身的 .harness/
|
|
769
|
+
const localHarness = path.join(projectDir, '.harness');
|
|
770
|
+
if (fs.existsSync(localHarness)) return localHarness;
|
|
771
|
+
|
|
772
|
+
// 2. npm 包内的 harness(当本包被安装后)
|
|
773
|
+
// __dirname 指向 lib/,向上两级到包根,再到 .harness/
|
|
774
|
+
const packageHarness = path.join(__dirname, '..', '..', '.harness');
|
|
775
|
+
if (fs.existsSync(packageHarness)) return packageHarness;
|
|
776
|
+
|
|
777
|
+
return null;
|
|
778
|
+
}
|