jsharness 1.0.1 → 1.1.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
@@ -4,11 +4,13 @@
4
4
  * jsharness CLI
5
5
  *
6
6
  * Usage:
7
- * npx jsharness init # 初始化(自动检测 AI 工具)
8
- * npx jsharness init --tool codebuddy # 指定工具
9
- * npx jsharness init --stack vue3 # 指定技术栈
10
- * npx jsharness list-tools # 列出支持的工具
11
- * npx jsharness status # 查看当前状态
7
+ * npx jsharness init # 交互式初始化(选择工具+技术栈)
8
+ * npx jsharness init --tool codebuddy # 指定工具(跳过工具选择)
9
+ * npx jsharness init --stack vue3 # 指定技术栈(跳过技术栈选择)
10
+ * npx jsharness init --tool cursor --stack all # 全部指定,无交互
11
+ * npx jsharness list-tools # 列出支持的工具
12
+ * npx jsharness status # 查看当前状态
13
+ * npx jsharness openspec list # 列出 OpenSpec 变更
12
14
  */
13
15
 
14
16
  import { createRequire } from 'module';
@@ -17,7 +19,7 @@ import { program } from 'commander';
17
19
  const require = createRequire(import.meta.url);
18
20
 
19
21
  // 通过包名引用主库(npm 安装后兼容)
20
- const { runInit, listTools, showStatus } = await import('jsharness');
22
+ const { runInit, listTools, showStatus, listOpenSpecChanges, archiveOpenSpecChange } = await import('jsharness');
21
23
 
22
24
  program
23
25
  .name('jsharness')
@@ -27,8 +29,8 @@ program
27
29
  program
28
30
  .command('init')
29
31
  .description('初始化 Harness 到当前项目的 AI 工具中')
30
- .option('-t, --tool <name>', '指定目标 AI 工具')
31
- .option('-s, --stack <name>', '指定技术栈 (vue3/java/all)', 'all')
32
+ .option('-t, --tool <name>', '指定目标 AI 工具(跳过交互选择)')
33
+ .option('-s, --stack <name>', '指定技术栈 vue3/java/all(跳过交互选择)')
32
34
  .option('--rules-only', '只注入规则,不注入技能')
33
35
  .option('--skills-only', '只注入技能,不注入规则')
34
36
  .option('--force', '覆盖已有配置')
@@ -45,6 +47,22 @@ program
45
47
  .description('查看当前项目 Harness 初始化状态')
46
48
  .action(() => showStatus(process.cwd()));
47
49
 
50
+ // OpenSpec 子命令
51
+ const openspecCmd = program
52
+ .command('openspec')
53
+ .description('OpenSpec 变更管理');
54
+
55
+ openspecCmd
56
+ .command('list')
57
+ .description('列出所有 OpenSpec 变更')
58
+ .action(() => listOpenSpecChanges(process.cwd()));
59
+
60
+ openspecCmd
61
+ .command('archive')
62
+ .description('归档一个已完成的 OpenSpec 变更')
63
+ .requiredOption('-c, --change <name>', '变更名称')
64
+ .action((opts) => archiveOpenSpecChange(process.cwd(), opts.change));
65
+
48
66
  program.parse(process.argv);
49
67
 
50
68
  // 无参数时默认执行 init
package/lib/index.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
+ import readline from 'readline';
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
 
@@ -595,10 +596,110 @@ export function injectOutputs(projectDir, outputs, options = {}) {
595
596
  // CLI 命令实现
596
597
  // ============================================================
597
598
 
599
+ // ============================================================
600
+ // 交互式问答辅助函数
601
+ // ============================================================
602
+
603
+ /**
604
+ * 通用 readline 提问
605
+ */
606
+ function askQuestion(query) {
607
+ const rl = readline.createInterface({
608
+ input: process.stdin,
609
+ output: process.stdout,
610
+ });
611
+ return new Promise((resolve) => {
612
+ rl.question(query, (answer) => {
613
+ rl.close();
614
+ resolve(answer.trim());
615
+ });
616
+ });
617
+ }
618
+
619
+ /**
620
+ * 交互式选择 AI 编程工具
621
+ *
622
+ * 先自动检测,检测结果让用户确认或修改;
623
+ * 检测不到则展示列表让用户选择(支持多选,逗号分隔)。
624
+ *
625
+ * @param {string} projectDir
626
+ * @returns {Promise<string[]>} 选中的工具 ID 列表
627
+ */
628
+ async function promptSelectTools(projectDir) {
629
+ const detected = detectTool(projectDir);
630
+
631
+ console.log('━━━ 选择 AI 编程工具 ━━━\n');
632
+
633
+ // 展示所有支持的工具
634
+ SUPPORTED_TOOLS.forEach((t, i) => {
635
+ const isDetected = detected.some(d => d.id === t.id);
636
+ console.log(` ${String(i + 1).padStart(2)}. ${isDetected ? '✅' : '○ '} ${t.name}`);
637
+ console.log(` ${t.description}`);
638
+ });
639
+
640
+ if (detected.length > 0) {
641
+ console.log(`\n 🔍 自动检测到: ${detected.map(t => t.name).join(', ')}`);
642
+ console.log(` 按回车使用检测结果,或输入编号选择其他工具\n`);
643
+ } else {
644
+ console.log('\n 未自动检测到 AI 工具,请手动选择\n');
645
+ }
646
+
647
+ const answer = await askQuestion(' 请选择 (多选用逗号分隔,如 1,3): ');
648
+
649
+ // 回车 = 使用检测结果
650
+ if (!answer && detected.length > 0) {
651
+ return detected.map(t => t.id);
652
+ }
653
+
654
+ // 解析用户输入的编号
655
+ const indices = answer.split(/[,,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
656
+ const selected = [];
657
+ for (const idx of indices) {
658
+ if (idx >= 1 && idx <= SUPPORTED_TOOLS.length) {
659
+ selected.push(SUPPORTED_TOOLS[idx - 1].id);
660
+ }
661
+ }
662
+
663
+ if (selected.length === 0) {
664
+ console.log(' ⚠️ 无有效选择,默认使用 CodeBuddy 格式\n');
665
+ return [SUPPORTED_TOOLS[0].id];
666
+ }
667
+
668
+ return selected;
669
+ }
670
+
671
+ /**
672
+ * 交互式选择技术栈
673
+ *
674
+ * @returns {Promise<string>} 'vue3' | 'java' | 'all'
675
+ */
676
+ async function promptSelectStack() {
677
+ console.log('━━━ 选择项目技术栈 ━━━\n');
678
+ console.log(' 1. 🖥️ 前端 (Vue3 + TypeScript + Element Plus)');
679
+ console.log(' 2. ☕ 后端 (Spring Boot + JDK21 + MyBatis-Plus)');
680
+ console.log(' 3. 🔗 前后端一起\n');
681
+
682
+ const answer = await askQuestion(' 请选择 [1/2/3]: ');
683
+
684
+ const map = { '1': 'vue3', '2': 'java', '3': 'all' };
685
+ return map[answer] || 'all';
686
+ }
687
+
688
+ function stackLabel(stack) {
689
+ const labels = {
690
+ vue3: '前端 (Vue3 + TypeScript + Element Plus)',
691
+ java: '后端 (Spring Boot + JDK21 + MyBatis-Plus)',
692
+ all: '前后端一起',
693
+ };
694
+ return labels[stack] || stack;
695
+ }
696
+
697
+
698
+
598
699
  export async function runInit(projectDir, options = {}) {
599
700
  const {
600
701
  tool: requestedTool,
601
- stack = 'all',
702
+ stack: requestedStack,
602
703
  rulesOnly = false,
603
704
  skillsOnly = false,
604
705
  force = false,
@@ -611,38 +712,54 @@ export async function runInit(projectDir, options = {}) {
611
712
  // 1. 确定 Harness 源路径
612
713
  const harnessDir = findHarnessSource(projectDir);
613
714
  if (!harnessDir) {
614
- console.error('❌ 未找到 .harness/ 目录。请确保在包含 .harness/ 的项目或包中运行。');
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) ? '是' : '否'}`);
615
726
  process.exit(1);
616
727
  }
617
728
  if (verbose) console.log(` 📂 Harness 源: ${harnessDir}`);
618
729
 
619
- // 2. 检测或指定 AI 工具
730
+ // 2. 交互式选择 AI 工具
620
731
  let targetTools = [];
621
- if (requestedTool) {
622
- const found = SUPPORTED_TOOLS.find(t => t.id === requestedTool || t.name.includes(requestedTool));
732
+ const selectedToolIds = requestedTool
733
+ ? [requestedTool]
734
+ : await promptSelectTools(projectDir);
735
+
736
+ for (const tid of selectedToolIds) {
737
+ const found = SUPPORTED_TOOLS.find(t => t.id === tid || t.name.includes(tid));
623
738
  if (found) {
624
- targetTools = [{ ...found, detected: true }];
625
- } else {
626
- console.error(`❌ 不支持的工具: ${requestedTool}`);
627
- console.log(` 可用工具: ${SUPPORTED_TOOLS.map(t => t.id).join(', ')}`);
628
- process.exit(1);
629
- }
630
- } else {
631
- targetTools = detectTool(projectDir);
632
- if (targetTools.length === 0) {
633
- console.log('⚠️ 未自动检测到 AI 工具。将使用默认模式(CodeBuddy 格式)。\n');
634
- console.log(' 提示: 使用 --tool <name> 指定工具,或运行 npx hariness list-tools 查看');
635
- targetTools = [{ ...SUPPORTED_TOOLS[0], detected: true }]; // 默认 CodeBuddy
739
+ targetTools.push({ ...found, detected: true });
636
740
  } else {
637
- console.log(`🔍 检测到的 AI 工具:`);
638
- for (const t of targetTools) {
639
- console.log(` • ${t.name} (${t.description})`);
640
- }
641
- console.log('');
741
+ console.warn(`⚠️ 不支持的工具: ${tid},已跳过`);
642
742
  }
643
743
  }
644
744
 
645
- // 3. 扫描源文件
745
+ if (targetTools.length === 0) {
746
+ console.error('❌ 未选择任何 AI 工具,初始化终止。');
747
+ process.exit(1);
748
+ }
749
+
750
+ console.log(`\n📋 选中的 AI 工具:`);
751
+ for (const t of targetTools) {
752
+ console.log(` • ${t.name}`);
753
+ }
754
+
755
+ // 3. 交互式选择技术栈
756
+ let stack = requestedStack;
757
+ if (!stack) {
758
+ stack = await promptSelectStack();
759
+ }
760
+ console.log(`\n🏗️ 技术栈: ${stackLabel(stack)}\n`);
761
+
762
+ // 4. 扫描源文件
646
763
  const allRuleFiles = scanHarnessRules(harnessDir, stack);
647
764
  const allSkillFiles = scanHarnessSkills(harnessDir, stack);
648
765
 
@@ -650,7 +767,7 @@ export async function runInit(projectDir, options = {}) {
650
767
  console.log(`📋 扫描结果: ${allRuleFiles.length} 个规则, ${allSkillFiles.length} 个技能\n`);
651
768
  }
652
769
 
653
- // 4. 对每个目标工具执行注入
770
+ // 5. 对每个目标工具执行注入
654
771
  const summary = [];
655
772
 
656
773
  for (const tool of targetTools) {
@@ -660,7 +777,7 @@ export async function runInit(projectDir, options = {}) {
660
777
 
661
778
  // 注入 Rules
662
779
  if (!skillsOnly && allRuleFiles.length > 0) {
663
- console.log(`\n📜 注入规则 (${allRuleFiles.length} 个)...`);
780
+ console.log(`📜 注入规则 (${allRuleFiles.length} 个)...`);
664
781
  const result = transformRules(allRuleFiles, tool.id, { stack });
665
782
  outputs.push(...result.files);
666
783
  }
@@ -678,7 +795,18 @@ export async function runInit(projectDir, options = {}) {
678
795
  summary.push({ tool: tool.name, written, skipped });
679
796
  }
680
797
 
681
- // 5. 输出总结
798
+ // 6. 初始化 OpenSpec 变更管理目录(内聚在 jsharness 中,无需额外依赖)
799
+ console.log('\n━━━ 初始化 OpenSpec ━━━');
800
+ const openspecResult = initOpenSpec(projectDir, { force, verbose });
801
+ if (openspecResult.created.length > 0) {
802
+ console.log(` ✅ 创建 ${openspecResult.created.length} 项:`);
803
+ openspecResult.created.forEach(f => console.log(` - ${f}`));
804
+ }
805
+ if (openspecResult.skipped.length > 0) {
806
+ console.log(` ⏭ 跳过 ${openspecResult.skipped.length} 项 (已存在)`);
807
+ }
808
+
809
+ // 7. 输出总结
682
810
  console.log('\n═════════════════════════════');
683
811
  console.log('✅ 初始化完成!');
684
812
 
@@ -694,11 +822,17 @@ export async function runInit(projectDir, options = {}) {
694
822
  }
695
823
  }
696
824
 
825
+ if (openspecResult.created.length > 0) {
826
+ console.log(`\n [OpenSpec]`);
827
+ console.log(` ✅ 创建 ${openspecResult.created.length} 项`);
828
+ }
829
+
697
830
  if (!force && summary.some(s => s.skipped.length > 0)) {
698
831
  console.log('\n💡 提示: 使用 --force 可覆盖已有配置');
699
832
  }
700
833
 
701
- console.log('\n🎉 Harness 规则已注入到你的 AI 工具中!开始对话即可生效。\n');
834
+ console.log('\n🎉 Harness 规则已注入到你的 AI 工具中!开始对话即可生效。');
835
+ console.log('📋 OpenSpec 变更管理已就绪,可使用 AI 工具的 openspec skill 创建变更。\n');
702
836
  }
703
837
 
704
838
  export function listTools() {
@@ -757,9 +891,201 @@ export function showStatus(projectDir) {
757
891
  console.log(` ${exists ? '✅' : '○ '} ${t.name.padEnd(12)} ${t.path}`);
758
892
  }
759
893
 
894
+ // OpenSpec 状态
895
+ const openspecDir = path.join(projectDir, 'openspec');
896
+ const hasOpenSpec = fs.existsSync(openspecDir);
897
+ console.log(`\n OpenSpec: ${hasOpenSpec ? '✅ 已初始化' : '○ 未初始化'}`);
898
+ if (hasOpenSpec) {
899
+ const changesDir = path.join(openspecDir, 'changes');
900
+ if (fs.existsSync(changesDir)) {
901
+ const active = fs.readdirSync(changesDir, { withFileTypes: true })
902
+ .filter(d => d.isDirectory() && d.name !== 'archive');
903
+ 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;
907
+ console.log(` 活跃变更: ${active.length} 归档: ${archived}`);
908
+ }
909
+ }
910
+
911
+ console.log('');
912
+ }
913
+
914
+ // ============================================================
915
+ // OpenSpec 内聚初始化(无需额外 npm 依赖,直接创建目录结构)
916
+ // ============================================================
917
+
918
+ const OPENSPEC_DIRS = [
919
+ 'openspec',
920
+ 'openspec/changes',
921
+ 'openspec/changes/archive',
922
+ 'openspec/specs',
923
+ ];
924
+
925
+ const OPENSPEC_GITIGNORE = `# OpenSpec generated files
926
+ changes/*/dist/
927
+ *.log
928
+ `;
929
+
930
+ /**
931
+ * 初始化 OpenSpec 目录结构到目标项目
932
+ *
933
+ * @param {string} projectDir - 目标项目根目录
934
+ * @param {object} options - 选项 { force, verbose }
935
+ * @returns {{ created: string[], skipped: string[] }}
936
+ */
937
+ export function initOpenSpec(projectDir, options = {}) {
938
+ const { force = false, verbose = false } = options;
939
+ const created = [];
940
+ const skipped = [];
941
+
942
+ for (const dir of OPENSPEC_DIRS) {
943
+ const fullPath = path.join(projectDir, dir);
944
+ if (fs.existsSync(fullPath)) {
945
+ if (verbose) console.log(` ⏭ 目录已存在: ${dir}/`);
946
+ skipped.push(dir);
947
+ continue;
948
+ }
949
+ fs.mkdirSync(fullPath, { recursive: true });
950
+ created.push(dir);
951
+ if (verbose) console.log(` ✅ 创建目录: ${dir}/`);
952
+ }
953
+
954
+ // .gitkeep 确保空目录可被 git 追踪
955
+ for (const dir of ['openspec/changes/archive', 'openspec/specs']) {
956
+ const gitkeepPath = path.join(projectDir, dir, '.gitkeep');
957
+ if (!fs.existsSync(gitkeepPath)) {
958
+ fs.writeFileSync(gitkeepPath, '', 'utf-8');
959
+ }
960
+ }
961
+
962
+ // openspec/.gitignore
963
+ const gitignorePath = path.join(projectDir, 'openspec', '.gitignore');
964
+ if (!fs.existsSync(gitignorePath) || force) {
965
+ fs.writeFileSync(gitignorePath, OPENSPEC_GITIGNORE, 'utf-8');
966
+ if (!created.includes('openspec')) created.push('openspec/.gitignore');
967
+ if (verbose) console.log(` ✅ 创建文件: openspec/.gitignore`);
968
+ } else {
969
+ skipped.push('openspec/.gitignore');
970
+ }
971
+
972
+ // openspec/README.md
973
+ const readmePath = path.join(projectDir, 'openspec', 'README.md');
974
+ if (!fs.existsSync(readmePath) || force) {
975
+ fs.writeFileSync(readmePath, generateOpenSpecReadme(), 'utf-8');
976
+ created.push('openspec/README.md');
977
+ if (verbose) console.log(` ✅ 创建文件: openspec/README.md`);
978
+ } else {
979
+ skipped.push('openspec/README.md');
980
+ }
981
+
982
+ return { created, skipped };
983
+ }
984
+
985
+ function generateOpenSpecReadme() {
986
+ return `# OpenSpec 变更管理
987
+
988
+ 本目录由 Harness Engineering 系统自动创建,用于管理结构化变更。
989
+
990
+ ## 目录结构
991
+
992
+ \`\`\`
993
+ openspec/
994
+ ├── changes/ # 活跃的变更(每个 change 一个子目录)
995
+ │ └── archive/ # 已归档的变更
996
+ └── specs/ # 功能规格定义
997
+ \`\`\`
998
+
999
+ ## 使用方式
1000
+
1001
+ 在 AI 对话中使用 OpenSpec skills:
1002
+
1003
+ - **openspec-propose** — 创建新变更提案
1004
+ - **openspec-explore** — 探索和澄清需求
1005
+ - **openspec-apply-change** — 实施变更任务
1006
+ - **openspec-archive-change** — 归档已完成变更
1007
+
1008
+ ## 命令行操作
1009
+
1010
+ \`\`\`bash
1011
+ # 查看状态
1012
+ npx jsharness status
1013
+
1014
+ # 列出活跃变更
1015
+ npx jsharness openspec list
1016
+
1017
+ # 归档变更
1018
+ npx jsharness openspec archive --change "<name>"
1019
+ \`\`\`
1020
+
1021
+ > 本目录由 Harness Engineering 系统自动管理。
1022
+ `;
1023
+ }
1024
+
1025
+ /**
1026
+ * 列出当前项目的 OpenSpec changes
1027
+ */
1028
+ export function listOpenSpecChanges(projectDir) {
1029
+ const changesDir = path.join(projectDir, 'openspec', 'changes');
1030
+ if (!fs.existsSync(changesDir)) {
1031
+ console.log('\n⚠️ OpenSpec 尚未初始化。请先运行: npx jsharness init\n');
1032
+ return;
1033
+ }
1034
+
1035
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true })
1036
+ .filter(d => d.isDirectory() && d.name !== 'archive');
1037
+
1038
+ if (entries.length === 0) {
1039
+ console.log('\n📋 没有活跃的 OpenSpec 变更\n');
1040
+ return;
1041
+ }
1042
+
1043
+ console.log('\n📋 OpenSpec 活跃变更:\n');
1044
+ for (const entry of entries) {
1045
+ const yamlPath = path.join(changesDir, entry.name, '.openspec.yaml');
1046
+ let status = '(未知)';
1047
+ if (fs.existsSync(yamlPath)) {
1048
+ try {
1049
+ const yaml = fs.readFileSync(yamlPath, 'utf-8');
1050
+ const m = yaml.match(/status:\s*(.+)/);
1051
+ if (m) status = m[1].trim();
1052
+ } catch { /* ignore */ }
1053
+ }
1054
+ console.log(` • ${entry.name} ${status}`);
1055
+ }
1056
+
1057
+ const archiveDir = path.join(changesDir, 'archive');
1058
+ if (fs.existsSync(archiveDir)) {
1059
+ const archived = fs.readdirSync(archiveDir, { withFileTypes: true })
1060
+ .filter(d => d.isDirectory());
1061
+ if (archived.length > 0) {
1062
+ console.log('\n📦 已归档:');
1063
+ for (const entry of archived) {
1064
+ console.log(` • ${entry.name}`);
1065
+ }
1066
+ }
1067
+ }
760
1068
  console.log('');
761
1069
  }
762
1070
 
1071
+ /**
1072
+ * 归档一个 OpenSpec change
1073
+ */
1074
+ export function archiveOpenSpecChange(projectDir, changeName) {
1075
+ const changesDir = path.join(projectDir, 'openspec', 'changes');
1076
+ const sourceDir = path.join(changesDir, changeName);
1077
+ const archiveDir = path.join(changesDir, 'archive', `${new Date().toISOString().split('T')[0]}-${changeName}`);
1078
+
1079
+ if (!fs.existsSync(sourceDir)) {
1080
+ console.error(`\n❌ 变更 "${changeName}" 不存在\n`);
1081
+ process.exit(1);
1082
+ }
1083
+
1084
+ fs.mkdirSync(path.join(changesDir, 'archive'), { recursive: true });
1085
+ fs.renameSync(sourceDir, archiveDir);
1086
+ console.log(`\n✅ 变更 "${changeName}" 已归档到 openspec/changes/archive/${path.basename(archiveDir)}/\n`);
1087
+ }
1088
+
763
1089
  // ============================================================
764
1090
  // 内部:查找 Harness 源
765
1091
  // ============================================================
@@ -769,20 +1095,45 @@ function findHarnessSource(projectDir) {
769
1095
  const localHarness = path.join(projectDir, '.harness');
770
1096
  if (fs.existsSync(localHarness)) return localHarness;
771
1097
 
772
- // 2. npm 包内的 harness — 通过 package.json 定位包根(兼容 npm install / global)
1098
+ // 2. 通过 node_modules 查找(npm i -D jsharness 后)
1099
+ const nmCandidates = [
1100
+ path.join(projectDir, 'node_modules', 'jsharness', '.harness'),
1101
+ path.join(projectDir, 'node_modules', '.pnpm', 'jsharness*', 'node_modules', 'jsharness', '.harness'),
1102
+ ];
1103
+ for (const c of nmCandidates) {
1104
+ try {
1105
+ const expanded = (c.includes('*') ? fs.readdirSync(path.dirname(c))
1106
+ .filter(d => d.startsWith('jsharness')).map(d => path.join(path.dirname(c), d, 'node_modules', 'jsharness', '.harness')) : [c])
1107
+ .flat();
1108
+ for (const p of expanded) {
1109
+ if (fs.existsSync(p)) return p;
1110
+ }
1111
+ } catch { /* skip */ }
1112
+ }
1113
+
1114
+ // 3. 通过 require.resolve 定位 npm 包根
773
1115
  try {
774
- const require = createRequire(import.meta.url);
775
- const pkgPath = require.resolve('jsharness/package.json');
776
- const pkgRoot = path.dirname(pkgPath);
777
- const packageHarness = path.join(pkgRoot, '.harness');
1116
+ const _require = createRequire(import.meta.url);
1117
+ const pkgPath = _require.resolve('jsharness/package.json');
1118
+ const packageHarness = path.join(path.dirname(pkgPath), '.harness');
778
1119
  if (fs.existsSync(packageHarness)) return packageHarness;
779
1120
  } catch {
780
- // jsharness 未作为依赖安装时跳过
1121
+ // require 解析失败,继续尝试其他方式
781
1122
  }
782
1123
 
783
- // 3. 本地开发模式 fallback(lib/ → 包根 → .harness/)
1124
+ // 4. __dirname 相对路径(本地开发模式)
784
1125
  const devHarness = path.join(__dirname, '..', '..', '.harness');
785
1126
  if (fs.existsSync(devHarness)) return devHarness;
786
1127
 
1128
+ // 5. 最后尝试:沿 __dirname 向上搜索 .harness
1129
+ let current = __dirname;
1130
+ for (let i = 0; i < 5; i++) {
1131
+ const candidate = path.join(current, '.harness');
1132
+ if (fs.existsSync(candidate) && current !== projectDir) return candidate;
1133
+ const parent = path.dirname(current);
1134
+ if (parent === current) break;
1135
+ current = parent;
1136
+ }
1137
+
787
1138
  return null;
788
1139
  }
package/package.json CHANGED
@@ -1 +1,53 @@
1
- {"name":"jsharness","version":"1.0.1","description":"Harness Engineering - AI 编程行为工程化管控系统。将 rules/skills/gate/agents 一键注入到 CodeBuddy、Cursor、Copilot 等 AI 工具中。","main":"lib/index.mjs","bin":{"jsharness":"./bin/jsharness.js"},"type":"module","scripts":{"jsharness":"node bin/jsharness.js","init":"node bin/jsharness.js init","list-tools":"node bin/jsharness.js list-tools","status":"node bin/jsharness.js status","test":"node bin/jsharness.js status"},"keywords":["harness","ai-coding","codebuddy","cursor","copilot","rules-engineering","ai-assistant","vue3","spring-boot"],"author":"","license":"ISC","engines":{"node":">=18.0.0"},"files":["bin/","lib/",".harness/","README.md"],"dependencies":{"commander":"^12.1.0"},"devDependencies":{"openspec":"^0.0.0","webpack":"^5.107.0","webpack-cli":"^7.0.2"},"repository":{"type":"git","url":""},"homepage":""}
1
+ {
2
+ "name": "jsharness",
3
+ "version": "1.1.0",
4
+ "description": "Harness Engineering - AI 编程行为工程化管控系统。将 rules/skills/gate/agents 一键注入到 CodeBuddy、Cursor、Copilot 等 AI 工具中。",
5
+ "main": "lib/index.mjs",
6
+ "bin": {
7
+ "jsharness": "./bin/jsharness.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "jsharness": "node bin/jsharness.js",
12
+ "init": "node bin/jsharness.js init",
13
+ "list-tools": "node bin/jsharness.js list-tools",
14
+ "status": "node bin/jsharness.js status",
15
+ "prepublishOnly": "echo 'Publishing jsharness to npm...'"
16
+ },
17
+ "keywords": [
18
+ "harness",
19
+ "ai-coding",
20
+ "codebuddy",
21
+ "cursor",
22
+ "copilot",
23
+ "claude",
24
+ "rules-engineering",
25
+ "ai-assistant",
26
+ "vue3",
27
+ "spring-boot",
28
+ "openspec"
29
+ ],
30
+ "author": "",
31
+ "license": "ISC",
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "files": [
36
+ "bin/",
37
+ "lib/",
38
+ ".harness/",
39
+ "README.md"
40
+ ],
41
+ "dependencies": {
42
+ "commander": "^12.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "webpack": "^5.107.0",
46
+ "webpack-cli": "^7.0.2"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": ""
51
+ },
52
+ "homepage": ""
53
+ }