jsharness 1.1.0 → 1.3.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/lib/index.mjs +237 -62
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -8,9 +8,20 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import readline from 'readline';
|
|
11
|
+
import https from 'https';
|
|
12
|
+
import zlib from 'zlib';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
11
14
|
|
|
12
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
16
|
|
|
17
|
+
// ============================================================
|
|
18
|
+
// 远端源配置(GitHub 仓库 + npm registry)
|
|
19
|
+
// ============================================================
|
|
20
|
+
|
|
21
|
+
const HARNESS_GITHUB_REPO = 'jieaiag/harness-engineering'; // TODO: 替换为实际仓库
|
|
22
|
+
const HARNESS_NPM_PACKAGE = 'jsharness';
|
|
23
|
+
const HARNESS_BRANCH = 'main';
|
|
24
|
+
|
|
14
25
|
// ============================================================
|
|
15
26
|
// 支持的 AI 工具清单
|
|
16
27
|
// ============================================================
|
|
@@ -497,6 +508,223 @@ function extractBody(markdown) {
|
|
|
497
508
|
// 扫描 Harness 源文件
|
|
498
509
|
// ============================================================
|
|
499
510
|
|
|
511
|
+
/**
|
|
512
|
+
* 获取 .harness/ 模板源目录
|
|
513
|
+
*
|
|
514
|
+
* 三级回退策略:
|
|
515
|
+
* 1. 本包自带(import.meta.url 定位)— npx 缓存正常时最快
|
|
516
|
+
* 2. npx 缓存解压目录 — 某些 npx 版本 .harness/ 未包含时
|
|
517
|
+
* 3. 从 npm registry 下载 tgz 并解压到临时目录 — 纯净环境兜底
|
|
518
|
+
*
|
|
519
|
+
* 类似 OpenSpec/SpecKit 的初始化方式:不依赖项目本地 node_modules,
|
|
520
|
+
* 而是从远端(npm registry)直接拉取模板文件写入目标项目。
|
|
521
|
+
*
|
|
522
|
+
* @param {object} [options]
|
|
523
|
+
* @param {boolean} [options.verbose]
|
|
524
|
+
* @returns {Promise<string|null>} .harness/ 目录的绝对路径,全部失败返回 null
|
|
525
|
+
*/
|
|
526
|
+
async function getHarnessSourceDir(options = {}) {
|
|
527
|
+
const { verbose = false } = options;
|
|
528
|
+
|
|
529
|
+
// ── 策略 1: 本包自带 .harness/(基于 import.meta.url)──
|
|
530
|
+
const libDir = path.dirname(fileURLToPath(import.meta.url));
|
|
531
|
+
const bundledDir = path.join(libDir, '..', '.harness');
|
|
532
|
+
if (fs.existsSync(bundledDir) && fs.existsSync(path.join(bundledDir, 'rules'))) {
|
|
533
|
+
if (verbose) console.log(` 📂 源=本包自带: ${bundledDir}`);
|
|
534
|
+
return bundledDir;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── 策略 2: npx 缓存中查找 jsharness 包 ──
|
|
538
|
+
try {
|
|
539
|
+
const npxCacheDir = findNpxCacheDir();
|
|
540
|
+
if (npxCacheDir) {
|
|
541
|
+
if (verbose) console.log(` 📂 源=npx缓存: ${npxCacheDir}`);
|
|
542
|
+
return npxCacheDir;
|
|
543
|
+
}
|
|
544
|
+
} catch { /* ignore */ }
|
|
545
|
+
|
|
546
|
+
// ── 策略 3: 从 npm registry 下载 jsharness tgz 并解压 ──
|
|
547
|
+
console.log(' ⬇️ 本地未找到 .harness/ 模板,从 npm 远端拉取...');
|
|
548
|
+
const remoteDir = await downloadAndExtractFromNpm(options);
|
|
549
|
+
if (remoteDir) {
|
|
550
|
+
if (verbose) console.log(` 📂 源=npm远端: ${remoteDir}`);
|
|
551
|
+
return remoteDir;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* 在 npx 缓存目录中搜索包含 .harness/ 的 jsharness 包
|
|
559
|
+
*/
|
|
560
|
+
function findNpxCacheDir() {
|
|
561
|
+
const candidates = [];
|
|
562
|
+
|
|
563
|
+
// npm 缓存根目录
|
|
564
|
+
try {
|
|
565
|
+
const npmCacheRoot = execSync('npm config get cache', { encoding: 'utf-8' }).trim();
|
|
566
|
+
candidates.push(path.join(npmCacheRoot, '_npx'));
|
|
567
|
+
} catch { /* ignore */ }
|
|
568
|
+
|
|
569
|
+
// 常见 npx 缓存路径
|
|
570
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || '';
|
|
571
|
+
if (homeDir) {
|
|
572
|
+
candidates.push(
|
|
573
|
+
path.join(homeDir, 'AppData', 'Local', 'npm-cache', '_npx'), // Windows
|
|
574
|
+
path.join(homeDir, '.npm', '_npx'), // Linux/macOS
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const cacheRoot of candidates) {
|
|
579
|
+
if (!fs.existsSync(cacheRoot)) continue;
|
|
580
|
+
|
|
581
|
+
// 递归搜索含 .harness/ 的 jsharness 包目录
|
|
582
|
+
const found = searchHarnessInDir(cacheRoot, 3);
|
|
583
|
+
if (found) return found;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* 在指定目录下递归搜索包含 .harness/rules/ 的目录
|
|
591
|
+
*/
|
|
592
|
+
function searchHarnessInDir(dir, maxDepth) {
|
|
593
|
+
if (maxDepth <= 0) return null;
|
|
594
|
+
try {
|
|
595
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
596
|
+
for (const entry of entries) {
|
|
597
|
+
if (!entry.isDirectory()) continue;
|
|
598
|
+
const fullPath = path.join(dir, entry.name);
|
|
599
|
+
|
|
600
|
+
// 检查是否包含 .harness/rules/
|
|
601
|
+
const harnessDir = path.join(fullPath, '.harness');
|
|
602
|
+
if (fs.existsSync(path.join(harnessDir, 'rules'))) {
|
|
603
|
+
return harnessDir;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 继续向下搜索
|
|
607
|
+
const found = searchHarnessInDir(fullPath, maxDepth - 1);
|
|
608
|
+
if (found) return found;
|
|
609
|
+
}
|
|
610
|
+
} catch { /* ignore permission errors */ }
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* 从 npm registry 下载 jsharness 包并解压 .harness/ 到临时目录
|
|
616
|
+
*/
|
|
617
|
+
async function downloadAndExtractFromNpm(options = {}) {
|
|
618
|
+
const { verbose = false } = options;
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
// 1. 获取包的 tgz URL
|
|
622
|
+
const packUrl = await getNpmPackageTarballUrl();
|
|
623
|
+
if (!packUrl) return null;
|
|
624
|
+
|
|
625
|
+
// 2. 下载 tgz 到临时目录
|
|
626
|
+
const tmpDir = path.join(process.env.TEMP || process.env.TMP || '/tmp', `jsharness-${Date.now()}`);
|
|
627
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
628
|
+
const tgzPath = path.join(tmpDir, 'jsharness.tgz');
|
|
629
|
+
|
|
630
|
+
if (verbose) console.log(` 📦 下载: ${packUrl}`);
|
|
631
|
+
await downloadFile(packUrl, tgzPath);
|
|
632
|
+
|
|
633
|
+
// 3. 解压(优先 tar 命令,回退到 Node.js 流式解压)
|
|
634
|
+
const extractDir = path.join(tmpDir, 'extracted');
|
|
635
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
execSync(`tar -xzf "${tgzPath}" -C "${extractDir}"`, { stdio: verbose ? 'inherit' : 'pipe' });
|
|
639
|
+
} catch {
|
|
640
|
+
// tar 命令失败(某些 Windows 环境),使用 Node.js 解压
|
|
641
|
+
if (verbose) console.log(' 📦 tar 命令失败,使用 Node.js 解压...');
|
|
642
|
+
await extractTgzWithNode(tgzPath, extractDir);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 解压后目录结构: package/.harness/ 或 .harness/
|
|
646
|
+
const harnessDir = path.join(extractDir, 'package', '.harness');
|
|
647
|
+
if (fs.existsSync(harnessDir)) return harnessDir;
|
|
648
|
+
|
|
649
|
+
const altDir = path.join(extractDir, '.harness');
|
|
650
|
+
if (fs.existsSync(altDir)) return altDir;
|
|
651
|
+
|
|
652
|
+
// 深度搜索
|
|
653
|
+
return searchHarnessInDir(extractDir, 2);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
if (verbose) console.log(` ⚠️ npm 远端下载失败: ${err.message}`);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* 使用 Node.js 内置模块解压 .tgz 文件
|
|
662
|
+
* .tgz = gzip(tar),使用 zlib + tar 命令组合
|
|
663
|
+
*/
|
|
664
|
+
async function extractTgzWithNode(tgzPath, destDir) {
|
|
665
|
+
// Windows 10+ 和 Linux/macOS 都自带 tar 命令
|
|
666
|
+
// 但 Windows 的 tar.exe 路径可能不同
|
|
667
|
+
const tarCommands = [
|
|
668
|
+
`tar -xzf "${tgzPath}" -C "${destDir}"`,
|
|
669
|
+
`cmd /c "tar -xzf \\"${tgzPath}\\" -C \\"${destDir}\\""`,
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
for (const cmd of tarCommands) {
|
|
673
|
+
try {
|
|
674
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
675
|
+
return; // 成功
|
|
676
|
+
} catch { /* 尝试下一种 */ }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
throw new Error('所有 tar 命令均失败,请确认系统安装了 tar');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* 获取 jsharness 包在 npm registry 上的 tarball URL
|
|
684
|
+
*/
|
|
685
|
+
async function getNpmPackageTarballUrl() {
|
|
686
|
+
const url = `https://registry.npmjs.org/${HARNESS_NPM_PACKAGE}/latest`;
|
|
687
|
+
const data = await httpGetJSON(url);
|
|
688
|
+
return data?.dist?.tarball || null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* HTTP GET 返回 JSON
|
|
693
|
+
*/
|
|
694
|
+
function httpGetJSON(url) {
|
|
695
|
+
return new Promise((resolve, reject) => {
|
|
696
|
+
https.get(url, { timeout: 10000 }, (res) => {
|
|
697
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
698
|
+
return httpGetJSON(res.headers.location).then(resolve, reject);
|
|
699
|
+
}
|
|
700
|
+
let body = '';
|
|
701
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
702
|
+
res.on('end', () => {
|
|
703
|
+
try { resolve(JSON.parse(body)); } catch { reject(new Error('Invalid JSON')); }
|
|
704
|
+
});
|
|
705
|
+
}).on('error', reject);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* 下载文件到本地路径
|
|
711
|
+
*/
|
|
712
|
+
function downloadFile(url, destPath) {
|
|
713
|
+
return new Promise((resolve, reject) => {
|
|
714
|
+
const file = fs.createWriteStream(destPath);
|
|
715
|
+
https.get(url, { timeout: 30000 }, (res) => {
|
|
716
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
717
|
+
return downloadFile(res.headers.location, destPath).then(resolve, reject);
|
|
718
|
+
}
|
|
719
|
+
res.pipe(file);
|
|
720
|
+
file.on('finish', () => { file.close(resolve); });
|
|
721
|
+
}).on('error', (err) => {
|
|
722
|
+
fs.unlinkSync(destPath);
|
|
723
|
+
reject(err);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
500
728
|
export function scanHarnessRules(harnessDir, stackFilter) {
|
|
501
729
|
const rulesDir = path.join(harnessDir, 'rules');
|
|
502
730
|
const results = [];
|
|
@@ -512,7 +740,6 @@ export function scanHarnessRules(harnessDir, stackFilter) {
|
|
|
512
740
|
if (stackFilter && stackFilter !== 'all') {
|
|
513
741
|
const lowerRel = relPath.toLowerCase();
|
|
514
742
|
if (stackFilter === 'vue3' && !lowerRel.includes('vue') && !lowerRel.includes('frontend') && !lowerRel.includes('global')) {
|
|
515
|
-
// 对于 vue3 过滤,保留 global 和 frontend/vue 相关
|
|
516
743
|
if (!lowerRel.includes('global') && !lowerRel.includes('frontend') && !lowerRel.includes('web')) {
|
|
517
744
|
continue;
|
|
518
745
|
}
|
|
@@ -709,20 +936,18 @@ export async function runInit(projectDir, options = {}) {
|
|
|
709
936
|
console.log('\n🔧 Harness Engineering 初始化');
|
|
710
937
|
console.log(` 目标目录: ${projectDir}\n`);
|
|
711
938
|
|
|
712
|
-
// 1.
|
|
713
|
-
const harnessDir =
|
|
939
|
+
// 1. 多源获取 .harness/ 模板(本包自带 → npx缓存 → npm远端)
|
|
940
|
+
const harnessDir = await getHarnessSourceDir({ verbose });
|
|
714
941
|
if (!harnessDir) {
|
|
715
|
-
console.error('❌
|
|
716
|
-
console.error('');
|
|
717
|
-
console.error(' 请先安装 jsharness:');
|
|
718
|
-
console.error(' npm i -D jsharness');
|
|
942
|
+
console.error('❌ 无法获取 .harness/ 模板。');
|
|
719
943
|
console.error('');
|
|
720
|
-
console.error('
|
|
721
|
-
console.error('
|
|
944
|
+
console.error(' 已尝试:');
|
|
945
|
+
console.error(' 1. 本包自带的 .harness/ 目录');
|
|
946
|
+
console.error(' 2. npx 缓存目录');
|
|
947
|
+
console.error(' 3. npm registry 远端下载');
|
|
722
948
|
console.error('');
|
|
723
|
-
|
|
724
|
-
console.error(
|
|
725
|
-
console.error(` [DEBUG] node_modules/jsharness/.harness 存在: ${fs.existsSync(nmPath) ? '是' : '否'}`);
|
|
949
|
+
console.error(' 请检查网络连接,或手动安装后重试:');
|
|
950
|
+
console.error(' npm install -g jsharness && jsharness init');
|
|
726
951
|
process.exit(1);
|
|
727
952
|
}
|
|
728
953
|
if (verbose) console.log(` 📂 Harness 源: ${harnessDir}`);
|
|
@@ -1086,54 +1311,4 @@ export function archiveOpenSpecChange(projectDir, changeName) {
|
|
|
1086
1311
|
console.log(`\n✅ 变更 "${changeName}" 已归档到 openspec/changes/archive/${path.basename(archiveDir)}/\n`);
|
|
1087
1312
|
}
|
|
1088
1313
|
|
|
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
1314
|
|
|
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 包根
|
|
1115
|
-
try {
|
|
1116
|
-
const _require = createRequire(import.meta.url);
|
|
1117
|
-
const pkgPath = _require.resolve('jsharness/package.json');
|
|
1118
|
-
const packageHarness = path.join(path.dirname(pkgPath), '.harness');
|
|
1119
|
-
if (fs.existsSync(packageHarness)) return packageHarness;
|
|
1120
|
-
} catch {
|
|
1121
|
-
// require 解析失败,继续尝试其他方式
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// 4. __dirname 相对路径(本地开发模式)
|
|
1125
|
-
const devHarness = path.join(__dirname, '..', '..', '.harness');
|
|
1126
|
-
if (fs.existsSync(devHarness)) return devHarness;
|
|
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
|
-
|
|
1138
|
-
return null;
|
|
1139
|
-
}
|