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 +26 -8
- package/lib/index.mjs +385 -34
- package/package.json +53 -1
package/bin/jsharness.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* jsharness CLI
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx jsharness init
|
|
8
|
-
* npx jsharness init --tool codebuddy
|
|
9
|
-
* npx jsharness init --stack vue3
|
|
10
|
-
* npx jsharness
|
|
11
|
-
* npx jsharness
|
|
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>', '指定技术栈
|
|
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
|
|
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/
|
|
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.
|
|
730
|
+
// 2. 交互式选择 AI 工具
|
|
620
731
|
let targetTools = [];
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
775
|
-
const pkgPath =
|
|
776
|
-
const
|
|
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
|
-
//
|
|
1121
|
+
// require 解析失败,继续尝试其他方式
|
|
781
1122
|
}
|
|
782
1123
|
|
|
783
|
-
//
|
|
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
|
-
{
|
|
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
|
+
}
|