jsharness 1.1.1 → 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.
Files changed (2) hide show
  1. package/lib/index.mjs +237 -64
  2. 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. 确定 Harness 源路径
713
- const harnessDir = findHarnessSource(projectDir);
939
+ // 1. 多源获取 .harness/ 模板(本包自带 → npx缓存 → npm远端)
940
+ const harnessDir = await getHarnessSourceDir({ verbose });
714
941
  if (!harnessDir) {
715
- console.error('❌ 未找到 .harness/ 目录。');
942
+ console.error('❌ 无法获取 .harness/ 模板。');
716
943
  console.error('');
717
- console.error(' 请先安装 jsharness:');
718
- console.error(' npm i -D jsharness');
944
+ console.error(' 已尝试:');
945
+ console.error(' 1. 本包自带的 .harness/ 目录');
946
+ console.error(' 2. npx 缓存目录');
947
+ console.error(' 3. npm registry 远端下载');
719
948
  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) ? '是' : '否'}`);
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,56 +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
1314
 
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.3.0",
4
4
  "description": "Harness Engineering - AI 编程行为工程化管控系统。将 rules/skills/gate/agents 一键注入到 CodeBuddy、Cursor、Copilot 等 AI 工具中。",
5
5
  "main": "lib/index.mjs",
6
6
  "bin": {