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