jsharness 1.0.2 → 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 +27 -9
- package/lib/index.mjs +342 -27
- package/package.json +53 -1
package/bin/jsharness.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
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,
|
|
@@ -619,7 +720,6 @@ export async function runInit(projectDir, options = {}) {
|
|
|
619
720
|
console.error(' 然后重新运行:');
|
|
620
721
|
console.error(' npx jsharness init --verbose');
|
|
621
722
|
console.error('');
|
|
622
|
-
// 输出调试信息
|
|
623
723
|
const nmPath = path.join(projectDir, 'node_modules', 'jsharness', '.harness');
|
|
624
724
|
console.error(` [DEBUG] 项目目录: ${projectDir}`);
|
|
625
725
|
console.error(` [DEBUG] node_modules/jsharness/.harness 存在: ${fs.existsSync(nmPath) ? '是' : '否'}`);
|
|
@@ -627,33 +727,39 @@ export async function runInit(projectDir, options = {}) {
|
|
|
627
727
|
}
|
|
628
728
|
if (verbose) console.log(` 📂 Harness 源: ${harnessDir}`);
|
|
629
729
|
|
|
630
|
-
// 2.
|
|
730
|
+
// 2. 交互式选择 AI 工具
|
|
631
731
|
let targetTools = [];
|
|
632
|
-
|
|
633
|
-
|
|
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));
|
|
634
738
|
if (found) {
|
|
635
|
-
targetTools
|
|
739
|
+
targetTools.push({ ...found, detected: true });
|
|
636
740
|
} else {
|
|
637
|
-
console.
|
|
638
|
-
console.log(` 可用工具: ${SUPPORTED_TOOLS.map(t => t.id).join(', ')}`);
|
|
639
|
-
process.exit(1);
|
|
640
|
-
}
|
|
641
|
-
} else {
|
|
642
|
-
targetTools = detectTool(projectDir);
|
|
643
|
-
if (targetTools.length === 0) {
|
|
644
|
-
console.log('⚠️ 未自动检测到 AI 工具。将使用默认模式(CodeBuddy 格式)。\n');
|
|
645
|
-
console.log(' 提示: 使用 --tool <name> 指定工具,或运行 npx hariness list-tools 查看');
|
|
646
|
-
targetTools = [{ ...SUPPORTED_TOOLS[0], detected: true }]; // 默认 CodeBuddy
|
|
647
|
-
} else {
|
|
648
|
-
console.log(`🔍 检测到的 AI 工具:`);
|
|
649
|
-
for (const t of targetTools) {
|
|
650
|
-
console.log(` • ${t.name} (${t.description})`);
|
|
651
|
-
}
|
|
652
|
-
console.log('');
|
|
741
|
+
console.warn(`⚠️ 不支持的工具: ${tid},已跳过`);
|
|
653
742
|
}
|
|
654
743
|
}
|
|
655
744
|
|
|
656
|
-
|
|
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. 扫描源文件
|
|
657
763
|
const allRuleFiles = scanHarnessRules(harnessDir, stack);
|
|
658
764
|
const allSkillFiles = scanHarnessSkills(harnessDir, stack);
|
|
659
765
|
|
|
@@ -661,7 +767,7 @@ export async function runInit(projectDir, options = {}) {
|
|
|
661
767
|
console.log(`📋 扫描结果: ${allRuleFiles.length} 个规则, ${allSkillFiles.length} 个技能\n`);
|
|
662
768
|
}
|
|
663
769
|
|
|
664
|
-
//
|
|
770
|
+
// 5. 对每个目标工具执行注入
|
|
665
771
|
const summary = [];
|
|
666
772
|
|
|
667
773
|
for (const tool of targetTools) {
|
|
@@ -671,7 +777,7 @@ export async function runInit(projectDir, options = {}) {
|
|
|
671
777
|
|
|
672
778
|
// 注入 Rules
|
|
673
779
|
if (!skillsOnly && allRuleFiles.length > 0) {
|
|
674
|
-
console.log(
|
|
780
|
+
console.log(`📜 注入规则 (${allRuleFiles.length} 个)...`);
|
|
675
781
|
const result = transformRules(allRuleFiles, tool.id, { stack });
|
|
676
782
|
outputs.push(...result.files);
|
|
677
783
|
}
|
|
@@ -689,7 +795,18 @@ export async function runInit(projectDir, options = {}) {
|
|
|
689
795
|
summary.push({ tool: tool.name, written, skipped });
|
|
690
796
|
}
|
|
691
797
|
|
|
692
|
-
//
|
|
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. 输出总结
|
|
693
810
|
console.log('\n═════════════════════════════');
|
|
694
811
|
console.log('✅ 初始化完成!');
|
|
695
812
|
|
|
@@ -705,11 +822,17 @@ export async function runInit(projectDir, options = {}) {
|
|
|
705
822
|
}
|
|
706
823
|
}
|
|
707
824
|
|
|
825
|
+
if (openspecResult.created.length > 0) {
|
|
826
|
+
console.log(`\n [OpenSpec]`);
|
|
827
|
+
console.log(` ✅ 创建 ${openspecResult.created.length} 项`);
|
|
828
|
+
}
|
|
829
|
+
|
|
708
830
|
if (!force && summary.some(s => s.skipped.length > 0)) {
|
|
709
831
|
console.log('\n💡 提示: 使用 --force 可覆盖已有配置');
|
|
710
832
|
}
|
|
711
833
|
|
|
712
|
-
console.log('\n🎉 Harness 规则已注入到你的 AI
|
|
834
|
+
console.log('\n🎉 Harness 规则已注入到你的 AI 工具中!开始对话即可生效。');
|
|
835
|
+
console.log('📋 OpenSpec 变更管理已就绪,可使用 AI 工具的 openspec skill 创建变更。\n');
|
|
713
836
|
}
|
|
714
837
|
|
|
715
838
|
export function listTools() {
|
|
@@ -768,9 +891,201 @@ export function showStatus(projectDir) {
|
|
|
768
891
|
console.log(` ${exists ? '✅' : '○ '} ${t.name.padEnd(12)} ${t.path}`);
|
|
769
892
|
}
|
|
770
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
|
+
|
|
771
911
|
console.log('');
|
|
772
912
|
}
|
|
773
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
|
+
}
|
|
1068
|
+
console.log('');
|
|
1069
|
+
}
|
|
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
|
+
|
|
774
1089
|
// ============================================================
|
|
775
1090
|
// 内部:查找 Harness 源
|
|
776
1091
|
// ============================================================
|
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
|
+
}
|