jsharness 1.1.1 → 1.4.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/bin/jsharness.js +20 -7
- package/lib/index.mjs +327 -414
- package/package.json +1 -1
package/bin/jsharness.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* jsharness CLI
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* npx jsharness init # 交互式初始化(选择工具+技术栈)
|
|
8
8
|
* npx jsharness init --tool codebuddy # 指定工具(跳过工具选择)
|
|
@@ -13,18 +13,32 @@
|
|
|
13
13
|
* npx jsharness openspec list # 列出 OpenSpec 变更
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
|
|
19
|
+
// 关键:用相对路径引用主库,不用 import('jsharness') 包名
|
|
20
|
+
// 这样在 npx 临时安装环境中也能可靠定位模块
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const libPath = path.join(__dirname, '..', 'lib', 'index.mjs');
|
|
18
23
|
|
|
24
|
+
const { runInit, listTools, showStatus, listOpenSpecChanges, archiveOpenSpecChange } = await import(libPath);
|
|
25
|
+
|
|
26
|
+
// 读取版本号
|
|
27
|
+
import { createRequire } from 'module';
|
|
19
28
|
const require = createRequire(import.meta.url);
|
|
29
|
+
let version = '0.0.0';
|
|
30
|
+
try {
|
|
31
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
32
|
+
version = pkg.version;
|
|
33
|
+
} catch { /* ignore */ }
|
|
20
34
|
|
|
21
|
-
//
|
|
22
|
-
const {
|
|
35
|
+
// commander 延迟导入,确保路径解析先完成
|
|
36
|
+
const { program } = await import('commander');
|
|
23
37
|
|
|
24
38
|
program
|
|
25
39
|
.name('jsharness')
|
|
26
40
|
.description('Harness Engineering - AI 编程行为工程化管控系统')
|
|
27
|
-
.version(
|
|
41
|
+
.version(version);
|
|
28
42
|
|
|
29
43
|
program
|
|
30
44
|
.command('init')
|
|
@@ -47,7 +61,6 @@ program
|
|
|
47
61
|
.description('查看当前项目 Harness 初始化状态')
|
|
48
62
|
.action(() => showStatus(process.cwd()));
|
|
49
63
|
|
|
50
|
-
// OpenSpec 子命令
|
|
51
64
|
const openspecCmd = program
|
|
52
65
|
.command('openspec')
|
|
53
66
|
.description('OpenSpec 变更管理');
|
package/lib/index.mjs
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Harness Engineering - Core Library
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* 多适配器架构:检测 AI 工具 → 转换规则格式 → 注入到对应位置
|
|
5
|
+
*
|
|
6
|
+
* v1.4.0: 多源获取策略,确保 npx jsharness init 在任何项目中 100% 可用
|
|
7
|
+
* 1. import.meta.url 定位本包 .harness/ (最快)
|
|
8
|
+
* 2. process.argv[1] 反推包根目录(npx 直接执行时)
|
|
9
|
+
* 3. npm 全局安装目录搜索
|
|
10
|
+
* 4. npm registry 远端下载 tgz 解压
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import fs from 'fs';
|
|
8
14
|
import path from 'path';
|
|
9
15
|
import { fileURLToPath } from 'url';
|
|
10
16
|
import readline from 'readline';
|
|
17
|
+
import https from 'https';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// 常量
|
|
22
|
+
// ============================================================
|
|
11
23
|
|
|
24
|
+
const HARNESS_NPM_PACKAGE = 'jsharness';
|
|
12
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
26
|
|
|
14
27
|
// ============================================================
|
|
@@ -20,7 +33,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
20
33
|
id: 'codebuddy',
|
|
21
34
|
name: 'CodeBuddy (腾讯云代码助手)',
|
|
22
35
|
description: '.codebuddy/rules/ + .codebuddy/skills/',
|
|
23
|
-
detector: hasDir('.codebuddy') || hasFile('CODEBUDDY.md'),
|
|
24
36
|
priority: 100,
|
|
25
37
|
ruleFormat: 'markdown',
|
|
26
38
|
skillFormat: 'markdown',
|
|
@@ -29,16 +41,14 @@ export const SUPPORTED_TOOLS = [
|
|
|
29
41
|
id: 'cursor',
|
|
30
42
|
name: 'Cursor',
|
|
31
43
|
description: '.cursorrules 文件',
|
|
32
|
-
detector: hasFile('.cursorrules'),
|
|
33
44
|
priority: 90,
|
|
34
45
|
ruleFormat: 'cursor-md',
|
|
35
|
-
skillFormat: null,
|
|
46
|
+
skillFormat: null,
|
|
36
47
|
},
|
|
37
48
|
{
|
|
38
49
|
id: 'copilot',
|
|
39
50
|
name: 'GitHub Copilot',
|
|
40
51
|
description: '.github/copilot-instructions.md',
|
|
41
|
-
detector: hasDir('.github'),
|
|
42
52
|
priority: 80,
|
|
43
53
|
ruleFormat: 'copilot-md',
|
|
44
54
|
skillFormat: null,
|
|
@@ -47,7 +57,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
47
57
|
id: 'windsurf',
|
|
48
58
|
name: 'Windsurf (Codeium)',
|
|
49
59
|
description: '.windsurfrules 目录',
|
|
50
|
-
detector: hasFile('.windsurfrules') || hasDir('.windsurf'),
|
|
51
60
|
priority: 70,
|
|
52
61
|
ruleFormat: 'windsurf-md',
|
|
53
62
|
skillFormat: null,
|
|
@@ -56,7 +65,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
56
65
|
id: 'continue',
|
|
57
66
|
name: 'Continue (VS Code)',
|
|
58
67
|
description: 'continue.config.json',
|
|
59
|
-
detector: hasFile('continue.config.json'),
|
|
60
68
|
priority: 60,
|
|
61
69
|
ruleFormat: 'continue-json',
|
|
62
70
|
skillFormat: null,
|
|
@@ -65,7 +73,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
65
73
|
id: 'cline',
|
|
66
74
|
name: 'Cline (VS Code)',
|
|
67
75
|
description: '.clinerules 文件',
|
|
68
|
-
detector: hasFile('.clinerules'),
|
|
69
76
|
priority: 50,
|
|
70
77
|
ruleFormat: 'cline-md',
|
|
71
78
|
skillFormat: null,
|
|
@@ -74,7 +81,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
74
81
|
id: 'qoder',
|
|
75
82
|
name: 'Qoder (阿里云)',
|
|
76
83
|
description: '.qoder/rules/ 目录',
|
|
77
|
-
detector: hasDir('.qoder'),
|
|
78
84
|
priority: 95,
|
|
79
85
|
ruleFormat: 'markdown',
|
|
80
86
|
skillFormat: 'markdown',
|
|
@@ -83,7 +89,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
83
89
|
id: 'codex',
|
|
84
90
|
name: 'Codex CLI (OpenAI)',
|
|
85
91
|
description: 'AGENTS.md 文件(项目根目录)',
|
|
86
|
-
detector: hasFile('AGENTS.md') || hasDir('.codex'),
|
|
87
92
|
priority: 85,
|
|
88
93
|
ruleFormat: 'agents-md',
|
|
89
94
|
skillFormat: null,
|
|
@@ -92,7 +97,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
92
97
|
id: 'claude-code',
|
|
93
98
|
name: 'Claude Code (Anthropic)',
|
|
94
99
|
description: '.claude/rules/ + CLAUDE.md',
|
|
95
|
-
detector: hasFile('CLAUDE.md') || hasDir('.claude'),
|
|
96
100
|
priority: 93,
|
|
97
101
|
ruleFormat: 'claude-rules-md',
|
|
98
102
|
skillFormat: 'claude-skills-md',
|
|
@@ -101,7 +105,6 @@ export const SUPPORTED_TOOLS = [
|
|
|
101
105
|
id: 'trae',
|
|
102
106
|
name: 'Trae (字节跳动)',
|
|
103
107
|
description: '.trae/rules/project_rules.md',
|
|
104
|
-
detector: hasDir('.trae') || hasFile('.trae/rules/project_rules.md'),
|
|
105
108
|
priority: 75,
|
|
106
109
|
ruleFormat: 'trae-rules-md',
|
|
107
110
|
skillFormat: null,
|
|
@@ -112,36 +115,35 @@ export const SUPPORTED_TOOLS = [
|
|
|
112
115
|
// 检测工具函数
|
|
113
116
|
// ============================================================
|
|
114
117
|
|
|
115
|
-
function hasDir(name) {
|
|
116
|
-
return () => fs.existsSync(name) && fs.lstatSync(name).isDirectory();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function hasFile(name) {
|
|
120
|
-
return () => fs.existsSync(name) && fs.lstatSync(name).isFile();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
118
|
/**
|
|
124
119
|
* 自动检测当前项目使用的 AI 工具
|
|
125
120
|
*/
|
|
126
121
|
export function detectTool(projectDir) {
|
|
122
|
+
const detectors = {
|
|
123
|
+
codebuddy: ['.codebuddy', 'CODEBUDDY.md'],
|
|
124
|
+
cursor: ['.cursorrules'],
|
|
125
|
+
copilot: ['.github'],
|
|
126
|
+
windsurf: ['.windsurfrules', '.windsurf'],
|
|
127
|
+
continue: ['continue.config.json'],
|
|
128
|
+
cline: ['.clinerules'],
|
|
129
|
+
qoder: ['.qoder'],
|
|
130
|
+
codex: ['AGENTS.md', '.codex'],
|
|
131
|
+
'claude-code': ['CLAUDE.md', '.claude'],
|
|
132
|
+
trae: ['.trae'],
|
|
133
|
+
};
|
|
134
|
+
|
|
127
135
|
const results = [];
|
|
128
|
-
|
|
129
136
|
for (const tool of SUPPORTED_TOOLS) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
: fs.existsSync(path.join(projectDir, typeof detectFn === 'string' ? detectFn : ''));
|
|
135
|
-
|
|
136
|
-
if (isDetected) {
|
|
137
|
+
const markers = detectors[tool.id] || [];
|
|
138
|
+
for (const marker of markers) {
|
|
139
|
+
const fullPath = path.join(projectDir, marker);
|
|
140
|
+
if (fs.existsSync(fullPath)) {
|
|
137
141
|
results.push({ ...tool, detected: true });
|
|
142
|
+
break;
|
|
138
143
|
}
|
|
139
|
-
} catch {
|
|
140
|
-
// 检测失败,跳过
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
// 按优先级排序,返回最匹配的
|
|
145
147
|
results.sort((a, b) => b.priority - a.priority);
|
|
146
148
|
return results;
|
|
147
149
|
}
|
|
@@ -150,17 +152,11 @@ export function detectTool(projectDir) {
|
|
|
150
152
|
// 规则转换引擎
|
|
151
153
|
// ============================================================
|
|
152
154
|
|
|
153
|
-
/**
|
|
154
|
-
* 将 .harness/rules/*.md 转换为目标工具的格式
|
|
155
|
-
*/
|
|
156
155
|
export function transformRules(ruleFiles, targetToolId, options = {}) {
|
|
157
156
|
const transformer = RULE_TRANSFORMERS[targetToolId] || RULE_TRANSFORMERS['generic'];
|
|
158
157
|
return transformer(ruleFiles, options);
|
|
159
158
|
}
|
|
160
159
|
|
|
161
|
-
/**
|
|
162
|
-
* 将 .harness/skills/*.md 转换为目标工具的格式
|
|
163
|
-
*/
|
|
164
160
|
export function transformSkills(skillFiles, targetToolId, options = {}) {
|
|
165
161
|
const transformer = SKILL_TRANSFORMERS[targetToolId] || SKILL_TRANSFORMERS['generic'];
|
|
166
162
|
return transformer(skillFiles, options);
|
|
@@ -172,169 +168,90 @@ export function transformSkills(skillFiles, targetToolId, options = {}) {
|
|
|
172
168
|
|
|
173
169
|
const RULE_TRANSFORMERS = {
|
|
174
170
|
|
|
175
|
-
/** CodeBuddy: 原样复制 .md 到 .codebuddy/rules/ */
|
|
176
171
|
codebuddy(ruleFiles, opts) {
|
|
177
172
|
const outputs = [];
|
|
178
173
|
for (const file of ruleFiles) {
|
|
179
174
|
const content = fs.readFileSync(file.path, 'utf-8');
|
|
180
|
-
outputs.push({
|
|
181
|
-
relativePath: `.codebuddy/rules/${file.name}`,
|
|
182
|
-
content,
|
|
183
|
-
source: file.path,
|
|
184
|
-
});
|
|
175
|
+
outputs.push({ relativePath: `.codebuddy/rules/${file.name}`, content, source: file.path });
|
|
185
176
|
}
|
|
186
177
|
return { format: 'directory', files: outputs };
|
|
187
178
|
},
|
|
188
179
|
|
|
189
|
-
/** Cursor: 合并所有规则为 .cursorrules */
|
|
190
180
|
cursor(ruleFiles, opts) {
|
|
191
|
-
let content = `# Harness Engineering Rules\n`;
|
|
192
|
-
content += `> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
193
|
-
|
|
181
|
+
let content = `# Harness Engineering Rules\n> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
194
182
|
for (const file of ruleFiles) {
|
|
195
|
-
|
|
196
|
-
content += extractBody(raw);
|
|
183
|
+
content += extractBody(fs.readFileSync(file.path, 'utf-8'));
|
|
197
184
|
content += '\n---\n\n';
|
|
198
185
|
}
|
|
199
|
-
|
|
200
186
|
return { format: 'single-file', files: [{ relativePath: '.cursorrules', content }] };
|
|
201
187
|
},
|
|
202
188
|
|
|
203
|
-
/** GitHub Copilot: 合并为 .github/copilot-instructions.md */
|
|
204
189
|
copilot(ruleFiles, opts) {
|
|
205
|
-
let content = `# Project Rules & Guidelines\n\n`;
|
|
206
|
-
content += `## Harness Engineering System\n\n`;
|
|
207
|
-
|
|
190
|
+
let content = `# Project Rules & Guidelines\n\n## Harness Engineering System\n\n`;
|
|
208
191
|
for (const file of ruleFiles) {
|
|
209
192
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
210
|
-
content += `### ${extractTitle(raw)}\n\n`;
|
|
211
|
-
content += extractBody(raw);
|
|
212
|
-
content += '\n\n';
|
|
193
|
+
content += `### ${extractTitle(raw)}\n\n${extractBody(raw)}\n\n`;
|
|
213
194
|
}
|
|
214
|
-
|
|
215
195
|
return { format: 'single-file', files: [{ relativePath: '.github/copilot-instructions.md', content }] };
|
|
216
196
|
},
|
|
217
197
|
|
|
218
|
-
/** Windsurf: .windsurfrules 格式 */
|
|
219
198
|
windsurf(ruleFiles, opts) {
|
|
220
199
|
let content = '';
|
|
221
200
|
for (const file of ruleFiles) {
|
|
222
201
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
223
|
-
content += `<rules>\n`;
|
|
224
|
-
content += `<rule name="${file.name.replace('.md', '')}">\n`;
|
|
225
|
-
content += extractBody(raw).trim();
|
|
226
|
-
content += `\n</rule>\n</rules>\n\n`;
|
|
202
|
+
content += `<rules>\n<rule name="${file.name.replace('.md', '')}">\n${extractBody(raw).trim()}\n</rule>\n</rules>\n\n`;
|
|
227
203
|
}
|
|
228
204
|
return { format: 'single-file', files: [{ relativePath: '.windsurfrules', content }] };
|
|
229
205
|
},
|
|
230
206
|
|
|
231
|
-
/** Continue: continue.config.json 格式 */
|
|
232
207
|
continue(ruleFiles, opts) {
|
|
233
|
-
const rules =
|
|
234
|
-
for (const file of ruleFiles) {
|
|
208
|
+
const rules = ruleFiles.map(file => {
|
|
235
209
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
description: extractDescription(raw),
|
|
240
|
-
content: extractBody(raw),
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
const config = { rules };
|
|
244
|
-
const content = JSON.stringify(config, null, 2);
|
|
245
|
-
return { format: 'json-config', files: [{ relativePath: 'continue.config.json', content }] };
|
|
210
|
+
return { name: file.name.replace('.md', ''), type: 'rule', description: extractDescription(raw), content: extractBody(raw) };
|
|
211
|
+
});
|
|
212
|
+
return { format: 'json-config', files: [{ relativePath: 'continue.config.json', content: JSON.stringify({ rules }, null, 2) }] };
|
|
246
213
|
},
|
|
247
214
|
|
|
248
|
-
/** Cline: .clinerules 格式(类似 Cursor) */
|
|
249
215
|
cline(ruleFiles, opts) {
|
|
250
216
|
let content = `# Harness Engineering Rules for Cline\n\n`;
|
|
251
217
|
for (const file of ruleFiles) {
|
|
252
218
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
253
|
-
content += `## ${extractTitle(raw)}\n\n`;
|
|
254
|
-
content += extractBody(raw);
|
|
255
|
-
content += '\n---\n\n';
|
|
219
|
+
content += `## ${extractTitle(raw)}\n\n${extractBody(raw)}\n---\n\n`;
|
|
256
220
|
}
|
|
257
221
|
return { format: 'single-file', files: [{ relativePath: '.clinerules', content }] };
|
|
258
222
|
},
|
|
259
223
|
|
|
260
|
-
/**
|
|
261
|
-
* ================================================================
|
|
262
|
-
* 新增工具适配器 (v1.1+)
|
|
263
|
-
* ================================================================
|
|
264
|
-
*/
|
|
265
|
-
|
|
266
|
-
/** Qoder: 原样复制 .md 到 .qoder/rules/ (与 CodeBuddy 相同模式) */
|
|
267
224
|
qoder(ruleFiles, opts) {
|
|
268
225
|
const outputs = [];
|
|
269
226
|
for (const file of ruleFiles) {
|
|
270
227
|
const content = fs.readFileSync(file.path, 'utf-8');
|
|
271
|
-
outputs.push({
|
|
272
|
-
relativePath: `.qoder/rules/${file.name}`,
|
|
273
|
-
content,
|
|
274
|
-
source: file.path,
|
|
275
|
-
});
|
|
228
|
+
outputs.push({ relativePath: `.qoder/rules/${file.name}`, content, source: file.path });
|
|
276
229
|
}
|
|
277
230
|
return { format: 'directory', files: outputs };
|
|
278
231
|
},
|
|
279
232
|
|
|
280
|
-
/** Codex CLI: 合并所有规则为 AGENTS.md(项目根目录) */
|
|
281
233
|
codex(ruleFiles, opts) {
|
|
282
|
-
let content = `# Harness Engineering - Project Instructions for Codex CLI\n`;
|
|
283
|
-
content +=
|
|
284
|
-
content += `> Source: .harness/ rules & skills system\n\n`;
|
|
285
|
-
|
|
286
|
-
// Codex AGENTS.md 推荐结构:Commands / Architecture / Conventions / Important
|
|
287
|
-
content += `## Overview\n\n`;
|
|
288
|
-
content += `This project uses **Harness Engineering** system with the following tech stack:\n\n`;
|
|
289
|
-
content += `- Frontend: Vue3 + Vite + TypeScript + Pinia + Element Plus\n`;
|
|
290
|
-
content += `- Backend: Spring Boot 3.2 + JDK21 + MyBatis-Plus + Nacos\n\n`;
|
|
291
|
-
|
|
292
|
-
content += `---\n\n## Conventions & Rules\n\n`;
|
|
293
|
-
|
|
234
|
+
let content = `# Harness Engineering - Project Instructions for Codex CLI\n> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n> Source: .harness/ rules & skills system\n\n`;
|
|
235
|
+
content += `## Overview\n\nThis project uses **Harness Engineering** system with the following tech stack:\n\n- Frontend: Vue3 + Vite + TypeScript + Pinia + Element Plus\n- Backend: Spring Boot 3.2 + JDK21 + MyBatis-Plus + Nacos\n\n---\n\n## Conventions & Rules\n\n`;
|
|
294
236
|
for (const file of ruleFiles) {
|
|
295
237
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
296
|
-
content += `### ${extractTitle(raw)}\n\n`;
|
|
297
|
-
content += extractBody(raw);
|
|
298
|
-
content += '\n\n';
|
|
238
|
+
content += `### ${extractTitle(raw)}\n\n${extractBody(raw)}\n\n`;
|
|
299
239
|
}
|
|
300
|
-
|
|
301
|
-
// 追加 Skills 摘要
|
|
302
|
-
content += `---\n\n## Available Build/Test Skills\n\n`;
|
|
303
|
-
content += 'See .harness/skills/ directory for detailed build, lint, test, and review procedures.\n';
|
|
304
|
-
content += `- \`npm run build\` — Vue3 frontend production build\n`;
|
|
305
|
-
content += `- \`mvn compile test\` — Java backend build with tests\n`;
|
|
306
|
-
content += `- \`node .harness/gate/index.js\` — Run gate compliance checks\n\n`;
|
|
307
|
-
|
|
240
|
+
content += `---\n\n## Available Build/Test Skills\n\nSee .harness/skills/ directory for detailed build, lint, test, and review procedures.\n- \`npm run build\` — Vue3 frontend production build\n- \`mvn compile test\` — Java backend build with tests\n- \`node .harness/gate/index.js\` — Run gate compliance checks\n\n`;
|
|
308
241
|
return { format: 'single-file', files: [{ relativePath: 'AGENTS.md', content }] };
|
|
309
242
|
},
|
|
310
243
|
|
|
311
|
-
/** Claude Code: 输出为 .claude/rules/*.md(支持 paths: frontmatter 路径限定) */
|
|
312
244
|
'claude-code'(ruleFiles, opts) {
|
|
313
245
|
const outputs = [];
|
|
314
246
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
claudeMd +=
|
|
318
|
-
claudeMd += `## Tech Stack\n`;
|
|
319
|
-
claudeMd += `- **Frontend**: Vue3 + Vite + TypeScript + Pinia + Element Plus\n`;
|
|
320
|
-
claudeMd += `- **Backend**: Spring Boot 3.2 (JDK21 Virtual Threads) + MyBatis-Plus\n\n`;
|
|
321
|
-
claudeMd += `## Commands\n`;
|
|
322
|
-
claudeMd += `\`\`\`bash\n`;
|
|
323
|
-
claudeMd += `npm run build # Frontend build\n`;
|
|
324
|
-
claudeMd += `npm run lint # ESLint check\n`;
|
|
325
|
-
claudeMd += `mvn compile test # Backend build + test\n`;
|
|
326
|
-
claudeMd += `node .harness/gate/index.js # Gate checks\n`;
|
|
327
|
-
claudeMd += `\`\`\`\n\n`;
|
|
328
|
-
claudeMd += `> Detailed rules are loaded from \`.claude/rules/\` files below.\n`;
|
|
329
|
-
|
|
247
|
+
let claudeMd = `# Harness Engineering System\n\n> Auto-generated by hariness. Full rules in \`.claude/rules/\`\n\n`;
|
|
248
|
+
claudeMd += `## Tech Stack\n- **Frontend**: Vue3 + Vite + TypeScript + Pinia + Element Plus\n- **Backend**: Spring Boot 3.2 (JDK21 Virtual Threads) + MyBatis-Plus\n\n`;
|
|
249
|
+
claudeMd += `## Commands\n\`\`\`bash\nnpm run build # Frontend build\nnpm run lint # ESLint check\nmvn compile test # Backend build + test\nnode .harness/gate/index.js # Gate checks\n\`\`\`\n\n> Detailed rules are loaded from \`.claude/rules/\` files below.\n`;
|
|
330
250
|
outputs.push({ relativePath: 'CLAUDE.md', content: claudeMd });
|
|
331
251
|
|
|
332
|
-
// 为每个规则文件生成独立的 .claude/rules/*.md,带路径限定 frontmatter
|
|
333
252
|
for (const file of ruleFiles) {
|
|
334
253
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
335
254
|
const body = extractBody(raw);
|
|
336
|
-
|
|
337
|
-
// 根据规则类别决定 paths 范围
|
|
338
255
|
let scopePaths = null;
|
|
339
256
|
if (file.category === 'project') {
|
|
340
257
|
if (file.relativePath.includes('vue3') || file.relativePath.includes('frontend') || file.relativePath.includes('web')) {
|
|
@@ -343,48 +260,28 @@ const RULE_TRANSFORMERS = {
|
|
|
343
260
|
scopePaths = ['src/main/**/*.java', '**/*.java'];
|
|
344
261
|
}
|
|
345
262
|
}
|
|
346
|
-
|
|
347
263
|
let content = '';
|
|
348
264
|
if (scopePaths) {
|
|
349
265
|
content += `---\npaths:\n`;
|
|
350
|
-
for (const p of scopePaths) {
|
|
351
|
-
content += ` - "${p}"\n`;
|
|
352
|
-
}
|
|
266
|
+
for (const p of scopePaths) content += ` - "${p}"\n`;
|
|
353
267
|
content += `---\n\n`;
|
|
354
268
|
}
|
|
355
|
-
|
|
356
269
|
content += body;
|
|
357
|
-
|
|
358
|
-
outputs.push({
|
|
359
|
-
relativePath: `.claude/rules/${file.name}`,
|
|
360
|
-
content,
|
|
361
|
-
source: file.path,
|
|
362
|
-
});
|
|
270
|
+
outputs.push({ relativePath: `.claude/rules/${file.name}`, content, source: file.path });
|
|
363
271
|
}
|
|
364
272
|
|
|
365
273
|
return { format: 'multi-file', files: outputs };
|
|
366
274
|
},
|
|
367
275
|
|
|
368
|
-
/** Trae: 合并为 .trae/rules/project_rules.md */
|
|
369
276
|
trae(ruleFiles, opts) {
|
|
370
|
-
let content = `# Harness Engineering Rules for Trae\n\n`;
|
|
371
|
-
content += `> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
372
|
-
content += `## 技术栈\n\n`;
|
|
373
|
-
content += `- 前端: Vue3 + Vite + TypeScript + Pinia + Element Plus\n`;
|
|
374
|
-
content += `- 后端: Spring Boot 3.2 + JDK21 + MyBatis-Plus\n\n`;
|
|
375
|
-
content += `---\n\n`;
|
|
376
|
-
|
|
277
|
+
let content = `# Harness Engineering Rules for Trae\n\n> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n\n## 技术栈\n\n- 前端: Vue3 + Vite + TypeScript + Pinia + Element Plus\n- 后端: Spring Boot 3.2 + JDK21 + MyBatis-Plus\n\n---\n\n`;
|
|
377
278
|
for (const file of ruleFiles) {
|
|
378
279
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
379
|
-
content += `### ${extractTitle(raw)}\n\n`;
|
|
380
|
-
content += extractBody(raw);
|
|
381
|
-
content += '\n\n---\n\n';
|
|
280
|
+
content += `### ${extractTitle(raw)}\n\n${extractBody(raw)}\n\n---\n\n`;
|
|
382
281
|
}
|
|
383
|
-
|
|
384
282
|
return { format: 'single-file', files: [{ relativePath: '.trae/rules/project_rules.md', content }] };
|
|
385
283
|
},
|
|
386
284
|
|
|
387
|
-
/** Generic fallback */
|
|
388
285
|
generic(ruleFiles, opts) {
|
|
389
286
|
return RULE_TRANSFORMERS.codebuddy(ruleFiles, opts);
|
|
390
287
|
},
|
|
@@ -392,74 +289,43 @@ const RULE_TRANSFORMERS = {
|
|
|
392
289
|
|
|
393
290
|
const SKILL_TRANSFORMERS = {
|
|
394
291
|
|
|
395
|
-
/** CodeBuddy: 复制到 .codebuddy/skills/ */
|
|
396
292
|
codebuddy(skillFiles, opts) {
|
|
397
293
|
const outputs = [];
|
|
398
294
|
for (const file of skillFiles) {
|
|
399
|
-
|
|
400
|
-
outputs.push({
|
|
401
|
-
relativePath: `.codebuddy/skills/${file.name}`,
|
|
402
|
-
content,
|
|
403
|
-
source: file.path,
|
|
404
|
-
});
|
|
295
|
+
outputs.push({ relativePath: `.codebuddy/skills/${file.name}`, content: fs.readFileSync(file.path, 'utf-8'), source: file.path });
|
|
405
296
|
}
|
|
406
297
|
return { format: 'directory', files: outputs };
|
|
407
298
|
},
|
|
408
299
|
|
|
409
|
-
/** 不支持 skills 的工具 → 合并到规则文件中 */
|
|
410
300
|
generic(skillFiles, opts) {
|
|
411
301
|
const outputs = [];
|
|
412
302
|
for (const file of skillFiles) {
|
|
413
|
-
|
|
414
|
-
outputs.push({
|
|
415
|
-
relativePath: `.harness/skills/${file.name}`, // 保持原始位置作为参考
|
|
416
|
-
content,
|
|
417
|
-
source: file.path,
|
|
418
|
-
});
|
|
303
|
+
outputs.push({ relativePath: `.harness/skills/${file.name}`, content: fs.readFileSync(file.path, 'utf-8'), source: file.path });
|
|
419
304
|
}
|
|
420
305
|
return { format: 'reference', files: outputs };
|
|
421
306
|
},
|
|
422
307
|
|
|
423
|
-
/** Qoder: 复制到 .qoder/skills/ */
|
|
424
308
|
qoder(skillFiles, opts) {
|
|
425
309
|
const outputs = [];
|
|
426
310
|
for (const file of skillFiles) {
|
|
427
|
-
|
|
428
|
-
outputs.push({
|
|
429
|
-
relativePath: `.qoder/skills/${file.name}`,
|
|
430
|
-
content,
|
|
431
|
-
source: file.path,
|
|
432
|
-
});
|
|
311
|
+
outputs.push({ relativePath: `.qoder/skills/${file.name}`, content: fs.readFileSync(file.path, 'utf-8'), source: file.path });
|
|
433
312
|
}
|
|
434
313
|
return { format: 'directory', files: outputs };
|
|
435
314
|
},
|
|
436
315
|
|
|
437
|
-
/** Claude Code: 复制到 .claude/skills/ (Claude 支持自动触发的 skills) */
|
|
438
316
|
'claude-code'(skillFiles, opts) {
|
|
439
317
|
const outputs = [];
|
|
440
318
|
for (const file of skillFiles) {
|
|
441
319
|
const raw = fs.readFileSync(file.path, 'utf-8');
|
|
442
320
|
const body = extractBody(raw);
|
|
443
|
-
|
|
444
|
-
// Claude Code skills 格式:YAML frontmatter + Markdown body
|
|
445
|
-
let content = `---\n`;
|
|
446
|
-
content += `name: ${file.name.replace('.md', '')}\n`;
|
|
447
|
-
content += `description: Harness Engineering Skill - ${extractDescription(raw) || file.name}\n`;
|
|
448
|
-
// 自动触发条件
|
|
321
|
+
let content = `---\nname: ${file.name.replace('.md', '')}\ndescription: Harness Engineering Skill - ${extractDescription(raw) || file.name}\n`;
|
|
449
322
|
const lowerName = file.name.toLowerCase();
|
|
450
323
|
if (lowerName.includes('build')) content += `trigger_type: manual\n`;
|
|
451
324
|
else if (lowerName.includes('test')) content += `trigger_type: on_save\n`;
|
|
452
325
|
else if (lowerName.includes('lint') || lowerName.includes('review')) content += `trigger_type: on_pr\n`;
|
|
453
326
|
else content += `trigger_type: manual\n`;
|
|
454
|
-
content += `---\n\n`;
|
|
455
|
-
|
|
456
|
-
content += body;
|
|
457
|
-
|
|
458
|
-
outputs.push({
|
|
459
|
-
relativePath: `.claude/skills/${file.name}`,
|
|
460
|
-
content,
|
|
461
|
-
source: file.path,
|
|
462
|
-
});
|
|
327
|
+
content += `---\n\n${body}`;
|
|
328
|
+
outputs.push({ relativePath: `.claude/skills/${file.name}`, content, source: file.path });
|
|
463
329
|
}
|
|
464
330
|
return { format: 'directory', files: outputs };
|
|
465
331
|
},
|
|
@@ -475,24 +341,249 @@ function extractTitle(markdown) {
|
|
|
475
341
|
}
|
|
476
342
|
|
|
477
343
|
function extractDescription(markdown) {
|
|
478
|
-
// 尝试提取第一段描述性文字
|
|
479
344
|
const body = markdown.replace(/^#.+/gm, '').trim();
|
|
480
345
|
const firstPara = body.match(/^(.+)/m);
|
|
481
346
|
return firstPara ? firstPara[1].slice(0, 120) : '';
|
|
482
347
|
}
|
|
483
348
|
|
|
484
349
|
function extractBody(markdown) {
|
|
485
|
-
// 移除 frontmatter (--- ... ---)
|
|
486
350
|
let body = markdown;
|
|
487
351
|
if (body.startsWith('---')) {
|
|
488
352
|
const endIdx = body.indexOf('---', 3);
|
|
489
|
-
if (endIdx > 0)
|
|
490
|
-
body = body.slice(endIdx + 3).trim();
|
|
491
|
-
}
|
|
353
|
+
if (endIdx > 0) body = body.slice(endIdx + 3).trim();
|
|
492
354
|
}
|
|
493
355
|
return body;
|
|
494
356
|
}
|
|
495
357
|
|
|
358
|
+
// ============================================================
|
|
359
|
+
// 多源获取 .harness/ 模板(核心:确保任何项目都能命中)
|
|
360
|
+
// ============================================================
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 获取 .harness/ 模板源目录
|
|
364
|
+
*
|
|
365
|
+
* 四级回退策略,确保 npx jsharness init 在任何环境下 100% 可用:
|
|
366
|
+
* 1. import.meta.url 定位本包 .harness/ — npx 缓存正常时最快
|
|
367
|
+
* 2. process.argv[1] / npm 全局安装目录反推 — 全局安装时
|
|
368
|
+
* 3. require.resolve 模块解析 — npm install 后
|
|
369
|
+
* 4. npm registry 远端下载 tgz 解压 — 纯净环境兜底
|
|
370
|
+
*/
|
|
371
|
+
async function getHarnessSourceDir(options = {}) {
|
|
372
|
+
const { verbose = false } = options;
|
|
373
|
+
const tried = [];
|
|
374
|
+
|
|
375
|
+
// ── 策略 1: import.meta.url 定位(npx 缓存中 lib/ 旁边就有 .harness/)──
|
|
376
|
+
const libDir = __dirname;
|
|
377
|
+
const bundledDir = path.join(libDir, '..', '.harness');
|
|
378
|
+
tried.push(`import.meta.url → ${bundledDir}`);
|
|
379
|
+
if (fs.existsSync(bundledDir) && fs.existsSync(path.join(bundledDir, 'rules'))) {
|
|
380
|
+
if (verbose) console.log(` 📂 源=本包自带: ${bundledDir}`);
|
|
381
|
+
return bundledDir;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── 策略 2: 从 CLI 入口文件反推包根目录 ──
|
|
385
|
+
// process.argv[1] = /path/to/npx-cache/.../node_modules/jsharness/bin/jsharness.js
|
|
386
|
+
// 包根 = bin/ 的上一级
|
|
387
|
+
try {
|
|
388
|
+
const cliPath = process.argv[1];
|
|
389
|
+
if (cliPath) {
|
|
390
|
+
const cliDir = path.dirname(path.resolve(cliPath));
|
|
391
|
+
// 可能是 bin/ 目录 → 上级就是包根
|
|
392
|
+
const parentDir = path.dirname(cliDir);
|
|
393
|
+
const hDir = path.join(parentDir, '.harness');
|
|
394
|
+
tried.push(`argv[1] → ${hDir}`);
|
|
395
|
+
if (fs.existsSync(hDir) && fs.existsSync(path.join(hDir, 'rules'))) {
|
|
396
|
+
if (verbose) console.log(` 📂 源=CLI入口反推: ${hDir}`);
|
|
397
|
+
return hDir;
|
|
398
|
+
}
|
|
399
|
+
// npx 有时结构是 node_modules/jsharness/bin/ → 向上两级
|
|
400
|
+
if (path.basename(cliDir) === 'bin') {
|
|
401
|
+
const hDir2 = path.join(path.dirname(parentDir), '.harness');
|
|
402
|
+
tried.push(`argv[1]+1 → ${hDir2}`);
|
|
403
|
+
if (fs.existsSync(hDir2) && fs.existsSync(path.join(hDir2, 'rules'))) {
|
|
404
|
+
if (verbose) console.log(` 📂 源=CLI入口反推(深): ${hDir2}`);
|
|
405
|
+
return hDir2;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch { /* ignore */ }
|
|
410
|
+
|
|
411
|
+
// ── 策略 3: npm 全局安装路径搜索 ──
|
|
412
|
+
try {
|
|
413
|
+
const globalDir = getNpmGlobalDir();
|
|
414
|
+
if (globalDir) {
|
|
415
|
+
const hDir = path.join(globalDir, 'node_modules', HARNESS_NPM_PACKAGE, '.harness');
|
|
416
|
+
tried.push(`npm全局 → ${hDir}`);
|
|
417
|
+
if (fs.existsSync(hDir) && fs.existsSync(path.join(hDir, 'rules'))) {
|
|
418
|
+
if (verbose) console.log(` 📂 源=npm全局: ${hDir}`);
|
|
419
|
+
return hDir;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch { /* ignore */ }
|
|
423
|
+
|
|
424
|
+
// ── 策略 4: 从 npm registry 下载 tgz 并解压 ──
|
|
425
|
+
console.log(' ⬇️ 本地未找到 .harness/ 模板,从 npm 远端拉取...');
|
|
426
|
+
try {
|
|
427
|
+
const remoteDir = await downloadAndExtractFromNpm(options);
|
|
428
|
+
if (remoteDir) {
|
|
429
|
+
if (verbose) console.log(` 📂 源=npm远端: ${remoteDir}`);
|
|
430
|
+
return remoteDir;
|
|
431
|
+
}
|
|
432
|
+
tried.push('npm远端下载 → 失败');
|
|
433
|
+
} catch (err) {
|
|
434
|
+
tried.push(`npm远端 → ${err.message}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── 全部失败 ──
|
|
438
|
+
console.error('\n❌ 无法获取 .harness/ 模板。已尝试的路径:');
|
|
439
|
+
for (const t of tried) console.error(` • ${t}`);
|
|
440
|
+
console.error('');
|
|
441
|
+
console.error(' 请尝试:');
|
|
442
|
+
console.error(' npm install -g jsharness && jsharness init');
|
|
443
|
+
console.error(' 或检查网络连接后重试:');
|
|
444
|
+
console.error(' npx jsharness@latest init');
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 获取 npm 全局安装根目录
|
|
450
|
+
*/
|
|
451
|
+
function getNpmGlobalDir() {
|
|
452
|
+
try {
|
|
453
|
+
return execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
454
|
+
} catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 从 npm registry 下载 jsharness 包并解压 .harness/ 到临时目录
|
|
461
|
+
*/
|
|
462
|
+
async function downloadAndExtractFromNpm(options = {}) {
|
|
463
|
+
const { verbose = false } = options;
|
|
464
|
+
|
|
465
|
+
// 1. 获取包的 tgz URL
|
|
466
|
+
const packUrl = await getNpmPackageTarballUrl();
|
|
467
|
+
if (!packUrl) return null;
|
|
468
|
+
|
|
469
|
+
// 2. 下载 tgz 到临时目录
|
|
470
|
+
const tmpDir = path.join(
|
|
471
|
+
process.env.TEMP || process.env.TMP || process.env.TMPDIR || '/tmp',
|
|
472
|
+
`jsharness-${Date.now()}`
|
|
473
|
+
);
|
|
474
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
475
|
+
const tgzPath = path.join(tmpDir, 'jsharness.tgz');
|
|
476
|
+
|
|
477
|
+
if (verbose) console.log(` 📦 下载: ${packUrl}`);
|
|
478
|
+
await downloadFile(packUrl, tgzPath);
|
|
479
|
+
|
|
480
|
+
// 3. 解压
|
|
481
|
+
const extractDir = path.join(tmpDir, 'extracted');
|
|
482
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
483
|
+
|
|
484
|
+
extractTgz(tgzPath, extractDir, verbose);
|
|
485
|
+
|
|
486
|
+
// 4. 查找 .harness/ 目录
|
|
487
|
+
const harnessDir = path.join(extractDir, 'package', '.harness');
|
|
488
|
+
if (fs.existsSync(harnessDir) && fs.existsSync(path.join(harnessDir, 'rules'))) {
|
|
489
|
+
return harnessDir;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 有些 tgz 没有 package/ 前缀
|
|
493
|
+
const altDir = path.join(extractDir, '.harness');
|
|
494
|
+
if (fs.existsSync(altDir) && fs.existsSync(path.join(altDir, 'rules'))) {
|
|
495
|
+
return altDir;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 深度搜索
|
|
499
|
+
return searchHarnessInDir(extractDir, 3);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 解压 tgz 文件(跨平台兼容)
|
|
504
|
+
*/
|
|
505
|
+
function extractTgz(tgzPath, destDir, verbose) {
|
|
506
|
+
const commands = [
|
|
507
|
+
`tar -xzf "${tgzPath}" -C "${destDir}"`,
|
|
508
|
+
];
|
|
509
|
+
// Windows 可能需要不同调用方式
|
|
510
|
+
if (process.platform === 'win32') {
|
|
511
|
+
commands.push(`cmd /c "tar -xzf \\"${tgzPath}\\" -C \\"${destDir}\\""`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const cmd of commands) {
|
|
515
|
+
try {
|
|
516
|
+
execSync(cmd, { stdio: verbose ? 'inherit' : 'pipe' });
|
|
517
|
+
return;
|
|
518
|
+
} catch { /* try next */ }
|
|
519
|
+
}
|
|
520
|
+
throw new Error('tar 解压失败,请确认系统安装了 tar 命令');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 在指定目录下递归搜索包含 .harness/rules/ 的目录
|
|
525
|
+
*/
|
|
526
|
+
function searchHarnessInDir(dir, maxDepth) {
|
|
527
|
+
if (maxDepth <= 0) return null;
|
|
528
|
+
try {
|
|
529
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
530
|
+
if (!entry.isDirectory()) continue;
|
|
531
|
+
const fullPath = path.join(dir, entry.name);
|
|
532
|
+
const rulesPath = path.join(fullPath, '.harness', 'rules');
|
|
533
|
+
if (fs.existsSync(rulesPath)) return path.join(fullPath, '.harness');
|
|
534
|
+
const found = searchHarnessInDir(fullPath, maxDepth - 1);
|
|
535
|
+
if (found) return found;
|
|
536
|
+
}
|
|
537
|
+
} catch { /* ignore */ }
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 获取 jsharness 包在 npm registry 上的 tarball URL
|
|
543
|
+
*/
|
|
544
|
+
async function getNpmPackageTarballUrl() {
|
|
545
|
+
const url = `https://registry.npmjs.org/${HARNESS_NPM_PACKAGE}/latest`;
|
|
546
|
+
const data = await httpGetJSON(url);
|
|
547
|
+
return data?.dist?.tarball || null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* HTTP GET 返回 JSON
|
|
552
|
+
*/
|
|
553
|
+
function httpGetJSON(url) {
|
|
554
|
+
return new Promise((resolve, reject) => {
|
|
555
|
+
https.get(url, { timeout: 10000 }, (res) => {
|
|
556
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
557
|
+
return httpGetJSON(res.headers.location).then(resolve, reject);
|
|
558
|
+
}
|
|
559
|
+
let body = '';
|
|
560
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
561
|
+
res.on('end', () => {
|
|
562
|
+
try { resolve(JSON.parse(body)); } catch { reject(new Error('Invalid JSON')); }
|
|
563
|
+
});
|
|
564
|
+
}).on('error', reject);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 下载文件到本地路径
|
|
570
|
+
*/
|
|
571
|
+
function downloadFile(url, destPath) {
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
const file = fs.createWriteStream(destPath);
|
|
574
|
+
https.get(url, { timeout: 30000 }, (res) => {
|
|
575
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
576
|
+
return downloadFile(res.headers.location, destPath).then(resolve, reject);
|
|
577
|
+
}
|
|
578
|
+
res.pipe(file);
|
|
579
|
+
file.on('finish', () => { file.close(resolve); });
|
|
580
|
+
}).on('error', (err) => {
|
|
581
|
+
try { fs.unlinkSync(destPath); } catch { /* ignore */ }
|
|
582
|
+
reject(err);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
496
587
|
// ============================================================
|
|
497
588
|
// 扫描 Harness 源文件
|
|
498
589
|
// ============================================================
|
|
@@ -511,25 +602,15 @@ export function scanHarnessRules(harnessDir, stackFilter) {
|
|
|
511
602
|
// 技术栈过滤
|
|
512
603
|
if (stackFilter && stackFilter !== 'all') {
|
|
513
604
|
const lowerRel = relPath.toLowerCase();
|
|
514
|
-
if (stackFilter === 'vue3' && !lowerRel.includes('vue') && !lowerRel.includes('frontend') && !lowerRel.includes('global')) {
|
|
515
|
-
|
|
516
|
-
if (!lowerRel.includes('global') && !lowerRel.includes('frontend') && !lowerRel.includes('web')) {
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
605
|
+
if (stackFilter === 'vue3' && !lowerRel.includes('vue') && !lowerRel.includes('frontend') && !lowerRel.includes('global') && !lowerRel.includes('web')) {
|
|
606
|
+
continue;
|
|
519
607
|
}
|
|
520
608
|
if (stackFilter === 'java' && !lowerRel.includes('java') && !lowerRel.includes('backend') && !lowerRel.includes('global')) {
|
|
521
|
-
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
609
|
+
continue;
|
|
524
610
|
}
|
|
525
611
|
}
|
|
526
612
|
|
|
527
|
-
results.push({
|
|
528
|
-
name: entry,
|
|
529
|
-
path: filePath,
|
|
530
|
-
category,
|
|
531
|
-
relativePath: relPath,
|
|
532
|
-
});
|
|
613
|
+
results.push({ name: entry, path: filePath, category, relativePath: relPath });
|
|
533
614
|
}
|
|
534
615
|
}
|
|
535
616
|
|
|
@@ -542,20 +623,12 @@ export function scanHarnessRules(harnessDir, stackFilter) {
|
|
|
542
623
|
export function scanHarnessSkills(harnessDir, stackFilter) {
|
|
543
624
|
const skillsDir = path.join(harnessDir, 'skills');
|
|
544
625
|
const results = [];
|
|
545
|
-
|
|
546
626
|
if (!fs.existsSync(skillsDir)) return results;
|
|
547
627
|
|
|
548
|
-
const
|
|
549
|
-
for (const entry of entries) {
|
|
628
|
+
for (const entry of fs.readdirSync(skillsDir).filter(f => f.endsWith('.md'))) {
|
|
550
629
|
const filePath = path.join(skillsDir, entry);
|
|
551
|
-
results.push({
|
|
552
|
-
name: entry,
|
|
553
|
-
path: filePath,
|
|
554
|
-
category: 'skill',
|
|
555
|
-
relativePath: path.relative(harnessDir, filePath),
|
|
556
|
-
});
|
|
630
|
+
results.push({ name: entry, path: filePath, category: 'skill', relativePath: path.relative(harnessDir, filePath) });
|
|
557
631
|
}
|
|
558
|
-
|
|
559
632
|
return results;
|
|
560
633
|
}
|
|
561
634
|
|
|
@@ -572,12 +645,10 @@ export function injectOutputs(projectDir, outputs, options = {}) {
|
|
|
572
645
|
const targetPath = path.join(projectDir, output.relativePath);
|
|
573
646
|
const targetDir = path.dirname(targetPath);
|
|
574
647
|
|
|
575
|
-
// 创建目录
|
|
576
648
|
if (!fs.existsSync(targetDir)) {
|
|
577
649
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
578
650
|
}
|
|
579
651
|
|
|
580
|
-
// 检查是否已存在
|
|
581
652
|
if (fs.existsSync(targetPath) && !force) {
|
|
582
653
|
skipped.push(output.relativePath);
|
|
583
654
|
if (verbose) console.log(` ⏭ 跳过 (已存在): ${output.relativePath}`);
|
|
@@ -592,45 +663,22 @@ export function injectOutputs(projectDir, outputs, options = {}) {
|
|
|
592
663
|
return { written, skipped };
|
|
593
664
|
}
|
|
594
665
|
|
|
595
|
-
// ============================================================
|
|
596
|
-
// CLI 命令实现
|
|
597
|
-
// ============================================================
|
|
598
|
-
|
|
599
666
|
// ============================================================
|
|
600
667
|
// 交互式问答辅助函数
|
|
601
668
|
// ============================================================
|
|
602
669
|
|
|
603
|
-
/**
|
|
604
|
-
* 通用 readline 提问
|
|
605
|
-
*/
|
|
606
670
|
function askQuestion(query) {
|
|
607
|
-
const rl = readline.createInterface({
|
|
608
|
-
input: process.stdin,
|
|
609
|
-
output: process.stdout,
|
|
610
|
-
});
|
|
671
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
611
672
|
return new Promise((resolve) => {
|
|
612
|
-
rl.question(query, (answer) => {
|
|
613
|
-
rl.close();
|
|
614
|
-
resolve(answer.trim());
|
|
615
|
-
});
|
|
673
|
+
rl.question(query, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
616
674
|
});
|
|
617
675
|
}
|
|
618
676
|
|
|
619
|
-
/**
|
|
620
|
-
* 交互式选择 AI 编程工具
|
|
621
|
-
*
|
|
622
|
-
* 先自动检测,检测结果让用户确认或修改;
|
|
623
|
-
* 检测不到则展示列表让用户选择(支持多选,逗号分隔)。
|
|
624
|
-
*
|
|
625
|
-
* @param {string} projectDir
|
|
626
|
-
* @returns {Promise<string[]>} 选中的工具 ID 列表
|
|
627
|
-
*/
|
|
628
677
|
async function promptSelectTools(projectDir) {
|
|
629
678
|
const detected = detectTool(projectDir);
|
|
630
679
|
|
|
631
680
|
console.log('━━━ 选择 AI 编程工具 ━━━\n');
|
|
632
681
|
|
|
633
|
-
// 展示所有支持的工具
|
|
634
682
|
SUPPORTED_TOOLS.forEach((t, i) => {
|
|
635
683
|
const isDetected = detected.some(d => d.id === t.id);
|
|
636
684
|
console.log(` ${String(i + 1).padStart(2)}. ${isDetected ? '✅' : '○ '} ${t.name}`);
|
|
@@ -646,18 +694,12 @@ async function promptSelectTools(projectDir) {
|
|
|
646
694
|
|
|
647
695
|
const answer = await askQuestion(' 请选择 (多选用逗号分隔,如 1,3): ');
|
|
648
696
|
|
|
649
|
-
|
|
650
|
-
if (!answer && detected.length > 0) {
|
|
651
|
-
return detected.map(t => t.id);
|
|
652
|
-
}
|
|
697
|
+
if (!answer && detected.length > 0) return detected.map(t => t.id);
|
|
653
698
|
|
|
654
|
-
// 解析用户输入的编号
|
|
655
699
|
const indices = answer.split(/[,,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
|
|
656
700
|
const selected = [];
|
|
657
701
|
for (const idx of indices) {
|
|
658
|
-
if (idx >= 1 && idx <= SUPPORTED_TOOLS.length)
|
|
659
|
-
selected.push(SUPPORTED_TOOLS[idx - 1].id);
|
|
660
|
-
}
|
|
702
|
+
if (idx >= 1 && idx <= SUPPORTED_TOOLS.length) selected.push(SUPPORTED_TOOLS[idx - 1].id);
|
|
661
703
|
}
|
|
662
704
|
|
|
663
705
|
if (selected.length === 0) {
|
|
@@ -668,11 +710,6 @@ async function promptSelectTools(projectDir) {
|
|
|
668
710
|
return selected;
|
|
669
711
|
}
|
|
670
712
|
|
|
671
|
-
/**
|
|
672
|
-
* 交互式选择技术栈
|
|
673
|
-
*
|
|
674
|
-
* @returns {Promise<string>} 'vue3' | 'java' | 'all'
|
|
675
|
-
*/
|
|
676
713
|
async function promptSelectStack() {
|
|
677
714
|
console.log('━━━ 选择项目技术栈 ━━━\n');
|
|
678
715
|
console.log(' 1. 🖥️ 前端 (Vue3 + TypeScript + Element Plus)');
|
|
@@ -680,7 +717,6 @@ async function promptSelectStack() {
|
|
|
680
717
|
console.log(' 3. 🔗 前后端一起\n');
|
|
681
718
|
|
|
682
719
|
const answer = await askQuestion(' 请选择 [1/2/3]: ');
|
|
683
|
-
|
|
684
720
|
const map = { '1': 'vue3', '2': 'java', '3': 'all' };
|
|
685
721
|
return map[answer] || 'all';
|
|
686
722
|
}
|
|
@@ -694,7 +730,9 @@ function stackLabel(stack) {
|
|
|
694
730
|
return labels[stack] || stack;
|
|
695
731
|
}
|
|
696
732
|
|
|
697
|
-
|
|
733
|
+
// ============================================================
|
|
734
|
+
// CLI 命令实现
|
|
735
|
+
// ============================================================
|
|
698
736
|
|
|
699
737
|
export async function runInit(projectDir, options = {}) {
|
|
700
738
|
const {
|
|
@@ -709,20 +747,9 @@ export async function runInit(projectDir, options = {}) {
|
|
|
709
747
|
console.log('\n🔧 Harness Engineering 初始化');
|
|
710
748
|
console.log(` 目标目录: ${projectDir}\n`);
|
|
711
749
|
|
|
712
|
-
// 1.
|
|
713
|
-
const harnessDir =
|
|
750
|
+
// 1. 多源获取 .harness/ 模板
|
|
751
|
+
const harnessDir = await getHarnessSourceDir({ verbose });
|
|
714
752
|
if (!harnessDir) {
|
|
715
|
-
console.error('❌ 未找到 .harness/ 目录。');
|
|
716
|
-
console.error('');
|
|
717
|
-
console.error(' 请先安装 jsharness:');
|
|
718
|
-
console.error(' npm i -D jsharness');
|
|
719
|
-
console.error('');
|
|
720
|
-
console.error(' 然后重新运行:');
|
|
721
|
-
console.error(' npx jsharness init --verbose');
|
|
722
|
-
console.error('');
|
|
723
|
-
const nmPath = path.join(projectDir, 'node_modules', 'jsharness', '.harness');
|
|
724
|
-
console.error(` [DEBUG] 项目目录: ${projectDir}`);
|
|
725
|
-
console.error(` [DEBUG] node_modules/jsharness/.harness 存在: ${fs.existsSync(nmPath) ? '是' : '否'}`);
|
|
726
753
|
process.exit(1);
|
|
727
754
|
}
|
|
728
755
|
if (verbose) console.log(` 📂 Harness 源: ${harnessDir}`);
|
|
@@ -748,15 +775,11 @@ export async function runInit(projectDir, options = {}) {
|
|
|
748
775
|
}
|
|
749
776
|
|
|
750
777
|
console.log(`\n📋 选中的 AI 工具:`);
|
|
751
|
-
for (const t of targetTools) {
|
|
752
|
-
console.log(` • ${t.name}`);
|
|
753
|
-
}
|
|
778
|
+
for (const t of targetTools) console.log(` • ${t.name}`);
|
|
754
779
|
|
|
755
780
|
// 3. 交互式选择技术栈
|
|
756
781
|
let stack = requestedStack;
|
|
757
|
-
if (!stack)
|
|
758
|
-
stack = await promptSelectStack();
|
|
759
|
-
}
|
|
782
|
+
if (!stack) stack = await promptSelectStack();
|
|
760
783
|
console.log(`\n🏗️ 技术栈: ${stackLabel(stack)}\n`);
|
|
761
784
|
|
|
762
785
|
// 4. 扫描源文件
|
|
@@ -775,27 +798,21 @@ export async function runInit(projectDir, options = {}) {
|
|
|
775
798
|
|
|
776
799
|
const outputs = [];
|
|
777
800
|
|
|
778
|
-
// 注入 Rules
|
|
779
801
|
if (!skillsOnly && allRuleFiles.length > 0) {
|
|
780
802
|
console.log(`📜 注入规则 (${allRuleFiles.length} 个)...`);
|
|
781
|
-
|
|
782
|
-
outputs.push(...result.files);
|
|
803
|
+
outputs.push(...transformRules(allRuleFiles, tool.id, { stack }).files);
|
|
783
804
|
}
|
|
784
805
|
|
|
785
|
-
// 注入 Skills
|
|
786
806
|
if (!rulesOnly && allSkillFiles.length > 0) {
|
|
787
807
|
console.log(`⚡ 注入技能 (${allSkillFiles.length} 个)...`);
|
|
788
|
-
|
|
789
|
-
outputs.push(...result.files);
|
|
808
|
+
outputs.push(...transformSkills(allSkillFiles, tool.id, { stack }).files);
|
|
790
809
|
}
|
|
791
810
|
|
|
792
|
-
// 写入文件
|
|
793
811
|
const { written, skipped } = injectOutputs(projectDir, outputs, { force, verbose });
|
|
794
|
-
|
|
795
812
|
summary.push({ tool: tool.name, written, skipped });
|
|
796
813
|
}
|
|
797
814
|
|
|
798
|
-
// 6. 初始化 OpenSpec
|
|
815
|
+
// 6. 初始化 OpenSpec
|
|
799
816
|
console.log('\n━━━ 初始化 OpenSpec ━━━');
|
|
800
817
|
const openspecResult = initOpenSpec(projectDir, { force, verbose });
|
|
801
818
|
if (openspecResult.created.length > 0) {
|
|
@@ -809,7 +826,7 @@ export async function runInit(projectDir, options = {}) {
|
|
|
809
826
|
// 7. 输出总结
|
|
810
827
|
console.log('\n═════════════════════════════');
|
|
811
828
|
console.log('✅ 初始化完成!');
|
|
812
|
-
|
|
829
|
+
|
|
813
830
|
for (const s of summary) {
|
|
814
831
|
if (s.written.length > 0) {
|
|
815
832
|
console.log(`\n [${s.tool}]`);
|
|
@@ -837,40 +854,30 @@ export async function runInit(projectDir, options = {}) {
|
|
|
837
854
|
|
|
838
855
|
export function listTools() {
|
|
839
856
|
console.log('\n支持的 AI 编程工具:\n');
|
|
840
|
-
|
|
841
857
|
for (const tool of SUPPORTED_TOOLS) {
|
|
842
|
-
|
|
843
|
-
console.log(`${status} ${tool.id.padEnd(14)} ${tool.name}`);
|
|
858
|
+
console.log(`○ ${tool.id.padEnd(14)} ${tool.name}`);
|
|
844
859
|
console.log(` 格式: ${tool.ruleFormat}${tool.skillFormat ? ' + ' + tool.skillFormat : ''}`);
|
|
845
|
-
console.log(` 说明: ${tool.description}`);
|
|
846
|
-
console.log('');
|
|
860
|
+
console.log(` 说明: ${tool.description}\n`);
|
|
847
861
|
}
|
|
848
|
-
|
|
849
862
|
console.log('使用方法:');
|
|
850
|
-
console.log(' npx
|
|
851
|
-
console.log('');
|
|
863
|
+
console.log(' npx jsharness init --tool <id>\n');
|
|
852
864
|
}
|
|
853
865
|
|
|
854
866
|
export function showStatus(projectDir) {
|
|
855
867
|
console.log('\n📊 Harness 项目状态\n');
|
|
856
868
|
|
|
857
|
-
// 检查 .harness 是否存在
|
|
858
869
|
const harnessDir = path.join(projectDir, '.harness');
|
|
859
870
|
const hasHarness = fs.existsSync(harnessDir);
|
|
860
871
|
console.log(` Harness 源: ${hasHarness ? '✅ 存在' : '❌ 缺失'} (.harness/)`);
|
|
861
872
|
|
|
862
873
|
if (hasHarness) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
console.log(` 规则数量: ${rulesCount}`);
|
|
866
|
-
console.log(` 技能数量: ${skillsCount}`);
|
|
874
|
+
console.log(` 规则数量: ${scanHarnessRules(harnessDir, 'all').length}`);
|
|
875
|
+
console.log(` 技能数量: ${scanHarnessSkills(harnessDir, 'all').length}`);
|
|
867
876
|
}
|
|
868
877
|
|
|
869
|
-
// 检查各工具状态
|
|
870
878
|
const detected = detectTool(projectDir);
|
|
871
879
|
console.log(` AI 工具: ${detected.length > 0 ? detected.map(t => t.name).join(', ') : '(未检测到)'}`);
|
|
872
880
|
|
|
873
|
-
// 检查已注入的目标文件
|
|
874
881
|
const targets = [
|
|
875
882
|
{ name: 'CodeBuddy', path: '.codebuddy/rules/' },
|
|
876
883
|
{ name: 'Qoder', path: '.qoder/rules/' },
|
|
@@ -886,54 +893,36 @@ export function showStatus(projectDir) {
|
|
|
886
893
|
|
|
887
894
|
console.log('\n 注入状态:');
|
|
888
895
|
for (const t of targets) {
|
|
889
|
-
const
|
|
890
|
-
const exists = fs.existsSync(fullPath);
|
|
896
|
+
const exists = fs.existsSync(path.join(projectDir, t.path));
|
|
891
897
|
console.log(` ${exists ? '✅' : '○ '} ${t.name.padEnd(12)} ${t.path}`);
|
|
892
898
|
}
|
|
893
899
|
|
|
894
|
-
// OpenSpec 状态
|
|
895
900
|
const openspecDir = path.join(projectDir, 'openspec');
|
|
896
901
|
const hasOpenSpec = fs.existsSync(openspecDir);
|
|
897
902
|
console.log(`\n OpenSpec: ${hasOpenSpec ? '✅ 已初始化' : '○ 未初始化'}`);
|
|
898
903
|
if (hasOpenSpec) {
|
|
899
904
|
const changesDir = path.join(openspecDir, 'changes');
|
|
900
905
|
if (fs.existsSync(changesDir)) {
|
|
901
|
-
const active = fs.readdirSync(changesDir, { withFileTypes: true })
|
|
902
|
-
.filter(d => d.isDirectory() && d.name !== 'archive');
|
|
906
|
+
const active = fs.readdirSync(changesDir, { withFileTypes: true }).filter(d => d.isDirectory() && d.name !== 'archive');
|
|
903
907
|
const archiveDir = path.join(changesDir, 'archive');
|
|
904
|
-
const archived = fs.existsSync(archiveDir)
|
|
905
|
-
? fs.readdirSync(archiveDir, { withFileTypes: true }).filter(d => d.isDirectory()).length
|
|
906
|
-
: 0;
|
|
908
|
+
const archived = fs.existsSync(archiveDir) ? fs.readdirSync(archiveDir, { withFileTypes: true }).filter(d => d.isDirectory()).length : 0;
|
|
907
909
|
console.log(` 活跃变更: ${active.length} 归档: ${archived}`);
|
|
908
910
|
}
|
|
909
911
|
}
|
|
910
|
-
|
|
911
912
|
console.log('');
|
|
912
913
|
}
|
|
913
914
|
|
|
914
915
|
// ============================================================
|
|
915
|
-
// OpenSpec
|
|
916
|
+
// OpenSpec 内聚初始化
|
|
916
917
|
// ============================================================
|
|
917
918
|
|
|
918
|
-
const OPENSPEC_DIRS = [
|
|
919
|
-
'openspec',
|
|
920
|
-
'openspec/changes',
|
|
921
|
-
'openspec/changes/archive',
|
|
922
|
-
'openspec/specs',
|
|
923
|
-
];
|
|
919
|
+
const OPENSPEC_DIRS = ['openspec', 'openspec/changes', 'openspec/changes/archive', 'openspec/specs'];
|
|
924
920
|
|
|
925
921
|
const OPENSPEC_GITIGNORE = `# OpenSpec generated files
|
|
926
922
|
changes/*/dist/
|
|
927
923
|
*.log
|
|
928
924
|
`;
|
|
929
925
|
|
|
930
|
-
/**
|
|
931
|
-
* 初始化 OpenSpec 目录结构到目标项目
|
|
932
|
-
*
|
|
933
|
-
* @param {string} projectDir - 目标项目根目录
|
|
934
|
-
* @param {object} options - 选项 { force, verbose }
|
|
935
|
-
* @returns {{ created: string[], skipped: string[] }}
|
|
936
|
-
*/
|
|
937
926
|
export function initOpenSpec(projectDir, options = {}) {
|
|
938
927
|
const { force = false, verbose = false } = options;
|
|
939
928
|
const created = [];
|
|
@@ -951,30 +940,23 @@ export function initOpenSpec(projectDir, options = {}) {
|
|
|
951
940
|
if (verbose) console.log(` ✅ 创建目录: ${dir}/`);
|
|
952
941
|
}
|
|
953
942
|
|
|
954
|
-
// .gitkeep 确保空目录可被 git 追踪
|
|
955
943
|
for (const dir of ['openspec/changes/archive', 'openspec/specs']) {
|
|
956
944
|
const gitkeepPath = path.join(projectDir, dir, '.gitkeep');
|
|
957
|
-
if (!fs.existsSync(gitkeepPath))
|
|
958
|
-
fs.writeFileSync(gitkeepPath, '', 'utf-8');
|
|
959
|
-
}
|
|
945
|
+
if (!fs.existsSync(gitkeepPath)) fs.writeFileSync(gitkeepPath, '', 'utf-8');
|
|
960
946
|
}
|
|
961
947
|
|
|
962
|
-
// openspec/.gitignore
|
|
963
948
|
const gitignorePath = path.join(projectDir, 'openspec', '.gitignore');
|
|
964
949
|
if (!fs.existsSync(gitignorePath) || force) {
|
|
965
950
|
fs.writeFileSync(gitignorePath, OPENSPEC_GITIGNORE, 'utf-8');
|
|
966
951
|
if (!created.includes('openspec')) created.push('openspec/.gitignore');
|
|
967
|
-
if (verbose) console.log(` ✅ 创建文件: openspec/.gitignore`);
|
|
968
952
|
} else {
|
|
969
953
|
skipped.push('openspec/.gitignore');
|
|
970
954
|
}
|
|
971
955
|
|
|
972
|
-
// openspec/README.md
|
|
973
956
|
const readmePath = path.join(projectDir, 'openspec', 'README.md');
|
|
974
957
|
if (!fs.existsSync(readmePath) || force) {
|
|
975
958
|
fs.writeFileSync(readmePath, generateOpenSpecReadme(), 'utf-8');
|
|
976
959
|
created.push('openspec/README.md');
|
|
977
|
-
if (verbose) console.log(` ✅ 创建文件: openspec/README.md`);
|
|
978
960
|
} else {
|
|
979
961
|
skipped.push('openspec/README.md');
|
|
980
962
|
}
|
|
@@ -1008,13 +990,8 @@ openspec/
|
|
|
1008
990
|
## 命令行操作
|
|
1009
991
|
|
|
1010
992
|
\`\`\`bash
|
|
1011
|
-
# 查看状态
|
|
1012
993
|
npx jsharness status
|
|
1013
|
-
|
|
1014
|
-
# 列出活跃变更
|
|
1015
994
|
npx jsharness openspec list
|
|
1016
|
-
|
|
1017
|
-
# 归档变更
|
|
1018
995
|
npx jsharness openspec archive --change "<name>"
|
|
1019
996
|
\`\`\`
|
|
1020
997
|
|
|
@@ -1022,9 +999,6 @@ npx jsharness openspec archive --change "<name>"
|
|
|
1022
999
|
`;
|
|
1023
1000
|
}
|
|
1024
1001
|
|
|
1025
|
-
/**
|
|
1026
|
-
* 列出当前项目的 OpenSpec changes
|
|
1027
|
-
*/
|
|
1028
1002
|
export function listOpenSpecChanges(projectDir) {
|
|
1029
1003
|
const changesDir = path.join(projectDir, 'openspec', 'changes');
|
|
1030
1004
|
if (!fs.existsSync(changesDir)) {
|
|
@@ -1032,8 +1006,7 @@ export function listOpenSpecChanges(projectDir) {
|
|
|
1032
1006
|
return;
|
|
1033
1007
|
}
|
|
1034
1008
|
|
|
1035
|
-
const entries = fs.readdirSync(changesDir, { withFileTypes: true })
|
|
1036
|
-
.filter(d => d.isDirectory() && d.name !== 'archive');
|
|
1009
|
+
const entries = fs.readdirSync(changesDir, { withFileTypes: true }).filter(d => d.isDirectory() && d.name !== 'archive');
|
|
1037
1010
|
|
|
1038
1011
|
if (entries.length === 0) {
|
|
1039
1012
|
console.log('\n📋 没有活跃的 OpenSpec 变更\n');
|
|
@@ -1056,21 +1029,15 @@ export function listOpenSpecChanges(projectDir) {
|
|
|
1056
1029
|
|
|
1057
1030
|
const archiveDir = path.join(changesDir, 'archive');
|
|
1058
1031
|
if (fs.existsSync(archiveDir)) {
|
|
1059
|
-
const archived = fs.readdirSync(archiveDir, { withFileTypes: true })
|
|
1060
|
-
.filter(d => d.isDirectory());
|
|
1032
|
+
const archived = fs.readdirSync(archiveDir, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
1061
1033
|
if (archived.length > 0) {
|
|
1062
1034
|
console.log('\n📦 已归档:');
|
|
1063
|
-
for (const entry of archived) {
|
|
1064
|
-
console.log(` • ${entry.name}`);
|
|
1065
|
-
}
|
|
1035
|
+
for (const entry of archived) console.log(` • ${entry.name}`);
|
|
1066
1036
|
}
|
|
1067
1037
|
}
|
|
1068
1038
|
console.log('');
|
|
1069
1039
|
}
|
|
1070
1040
|
|
|
1071
|
-
/**
|
|
1072
|
-
* 归档一个 OpenSpec change
|
|
1073
|
-
*/
|
|
1074
1041
|
export function archiveOpenSpecChange(projectDir, changeName) {
|
|
1075
1042
|
const changesDir = path.join(projectDir, 'openspec', 'changes');
|
|
1076
1043
|
const sourceDir = path.join(changesDir, changeName);
|
|
@@ -1085,57 +1052,3 @@ export function archiveOpenSpecChange(projectDir, changeName) {
|
|
|
1085
1052
|
fs.renameSync(sourceDir, archiveDir);
|
|
1086
1053
|
console.log(`\n✅ 变更 "${changeName}" 已归档到 openspec/changes/archive/${path.basename(archiveDir)}/\n`);
|
|
1087
1054
|
}
|
|
1088
|
-
|
|
1089
|
-
// ============================================================
|
|
1090
|
-
// 内部:查找 Harness 源
|
|
1091
|
-
// ============================================================
|
|
1092
|
-
|
|
1093
|
-
function findHarnessSource(projectDir) {
|
|
1094
|
-
// 1. 项目自身的 .harness/
|
|
1095
|
-
const localHarness = path.join(projectDir, '.harness');
|
|
1096
|
-
if (fs.existsSync(localHarness)) return localHarness;
|
|
1097
|
-
|
|
1098
|
-
// 2. 本包自身的 .harness/(最可靠:基于 lib/index.mjs 的位置定位)
|
|
1099
|
-
// lib/index.mjs 所在目录 = <pkg_root>/lib/
|
|
1100
|
-
// 所以 .harness/ 在 <pkg_root>/.harness/ 即 __dirname/../.harness
|
|
1101
|
-
const selfHarness = path.join(__dirname, '..', '.harness');
|
|
1102
|
-
if (fs.existsSync(selfHarness)) return selfHarness;
|
|
1103
|
-
|
|
1104
|
-
// 3. 通过 node_modules 查找(npm i -D jsharness 后)
|
|
1105
|
-
const nmCandidates = [
|
|
1106
|
-
path.join(projectDir, 'node_modules', 'jsharness', '.harness'),
|
|
1107
|
-
path.join(projectDir, 'node_modules', '.pnpm', 'jsharness*', 'node_modules', 'jsharness', '.harness'),
|
|
1108
|
-
];
|
|
1109
|
-
for (const c of nmCandidates) {
|
|
1110
|
-
try {
|
|
1111
|
-
const expanded = (c.includes('*') ? fs.readdirSync(path.dirname(c))
|
|
1112
|
-
.filter(d => d.startsWith('jsharness')).map(d => path.join(path.dirname(c), d, 'node_modules', 'jsharness', '.harness')) : [c])
|
|
1113
|
-
.flat();
|
|
1114
|
-
for (const p of expanded) {
|
|
1115
|
-
if (fs.existsSync(p)) return p;
|
|
1116
|
-
}
|
|
1117
|
-
} catch { /* skip */ }
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// 4. 通过 require.resolve 定位 npm 包根
|
|
1121
|
-
try {
|
|
1122
|
-
const _require = createRequire(import.meta.url);
|
|
1123
|
-
const pkgPath = _require.resolve('jsharness/package.json');
|
|
1124
|
-
const packageHarness = path.join(path.dirname(pkgPath), '.harness');
|
|
1125
|
-
if (fs.existsSync(packageHarness)) return packageHarness;
|
|
1126
|
-
} catch {
|
|
1127
|
-
// require 解析失败,继续尝试其他方式
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// 5. 最后尝试:沿 __dirname 向上搜索 .harness
|
|
1131
|
-
let current = __dirname;
|
|
1132
|
-
for (let i = 0; i < 5; i++) {
|
|
1133
|
-
const candidate = path.join(current, '.harness');
|
|
1134
|
-
if (fs.existsSync(candidate) && current !== projectDir) return candidate;
|
|
1135
|
-
const parent = path.dirname(current);
|
|
1136
|
-
if (parent === current) break;
|
|
1137
|
-
current = parent;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|