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 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,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, // Cursor 不支持独立 skills
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
- try {
131
- const detectFn = tool.detector;
132
- const isDetected = typeof detectFn === 'function'
133
- ? detectFn()
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
- const raw = fs.readFileSync(file.path, 'utf-8');
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
- rules.push({
237
- name: file.name.replace('.md', ''),
238
- type: 'rule',
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 += `> Generated by hariness on ${new Date().toISOString().split('T')[0]}\n`;
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
- // 生成 CLAUDE.md 主文件(精简版,<200行)
316
- let claudeMd = `# Harness Engineering System\n\n`;
317
- claudeMd += `> Auto-generated by hariness. Full rules in \`.claude/rules/\`\n\n`;
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
- const content = fs.readFileSync(file.path, 'utf-8');
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
- const content = fs.readFileSync(file.path, 'utf-8');
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
- const content = fs.readFileSync(file.path, 'utf-8');
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
- // 对于 vue3 过滤,保留 global 和 frontend/vue 相关
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
- if (!lowerRel.includes('global') && !lowerRel.includes('backend')) {
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 entries = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md'));
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. 确定 Harness 源路径
713
- const harnessDir = findHarnessSource(projectDir);
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
- const result = transformRules(allRuleFiles, tool.id, { stack });
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
- const result = transformSkills(allSkillFiles, tool.id, { stack });
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 变更管理目录(内聚在 jsharness 中,无需额外依赖)
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
- const status = tool.detector ? '✅' : '○ ';
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 hariness init --tool <id>');
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
- const rulesCount = scanHarnessRules(harnessDir, 'all').length;
864
- const skillsCount = scanHarnessSkills(harnessDir, 'all').length;
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 fullPath = path.join(projectDir, t.path);
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 内聚初始化(无需额外 npm 依赖,直接创建目录结构)
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsharness",
3
- "version": "1.1.1",
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": {