@andyqiu/codeforge 0.6.5 → 0.6.7

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/install.mjs CHANGED
@@ -492,15 +492,33 @@ function installMdDirs({ targetRoot }) {
492
492
  const pruned = pruneStale({ dstPath, prevList, sourceSet })
493
493
 
494
494
  // 步骤 4:upsert —— 把 sourceSet 文件复制到目标(覆盖同名,不动其他文件)
495
+ // ADR:install-manifest-actual-copied-consistency —— manifest 记录实际落盘(事实),非源扫描意图
495
496
  let count = 0
496
- for (const base of sourceFiles) {
497
- const f = path.join(srcPath, base)
498
- run(`cp ${f} ${dstPath}/`, () => fs.copyFileSync(f, path.join(dstPath, base)))
499
- count++
497
+ const copiedOk = [] // 仅含 post-copy existsSync 复核通过的文件名
498
+ if (!DRY_RUN) {
499
+ // 真实 copy:收集 copiedOk,逐文件复核,复核失败 fail-loud
500
+ for (const base of sourceFiles) {
501
+ const src = path.join(srcPath, base)
502
+ const dst = path.join(dstPath, base)
503
+ fs.copyFileSync(src, dst)
504
+ if (!fs.existsSync(dst)) {
505
+ // copy 后文件仍不存在:可能是 FS 异常(NFS/虚拟 FS 等),fail-loud
506
+ throw new Error(`copy 后文件不存在(existsSync 复核失败):${dst}`)
507
+ }
508
+ copiedOk.push(base)
509
+ count++
510
+ }
511
+ } else {
512
+ // DRY_RUN:run() 已 vlog,不执行 copy,copiedOk 保持空(dry-run 不落盘)
513
+ for (const base of sourceFiles) {
514
+ vlog(`[dry-run] cp ${path.join(srcPath, base)} ${dstPath}/`)
515
+ count++
516
+ }
500
517
  }
501
518
 
502
- // 步骤 5:新 manifest = sourceSet(本版实际安装的文件名)
503
- nextManifest[dstName] = [...sourceFiles]
519
+ // 步骤 5:新 manifest = copiedOk(实际落盘文件,非 sourceFiles 意图)
520
+ // DRY_RUN 下 copiedOk 为空,由 run() 包裹的 writeMdManifest 负责 vlog 不落盘
521
+ nextManifest[dstName] = DRY_RUN ? [...sourceFiles] : copiedOk
504
522
 
505
523
  vok(`${srcName}/ → ${dstPath} (${count} 个 .md upsert, ${pruned} 个过期清理, 第三方文件保留)`)
506
524
  }
@@ -551,6 +569,77 @@ function writeMdManifest(targetRoot, data) {
551
569
  atomicWriteJson(p, data)
552
570
  }
553
571
 
572
+ // compareManifestVsDisk:三方比对(manifest / disk / source)
573
+ // ADR:codeforge-doctor-command — doctor 与 install 共享的纯函数
574
+ // 返回:
575
+ // missing[] = manifest 登记但 disk 无(来源2:外部腐蚀)
576
+ // unmanaged[] = source 有(经 shouldCopyMd 过滤)但 disk 无(来源1/2:未安装 / 被删)
577
+ // stale[] = disk 有 & source 有 但 manifest 无(manifest 失配,可能未更新)
578
+ // 第三方文件(disk 有但不在 manifest 也不在 source)→ 不纳入任何桶(静默跳过)
579
+ function compareManifestVsDisk({ targetRoot, sourceRoot }) {
580
+ const manifest = readMdManifest(targetRoot)
581
+ const missing = []
582
+ const unmanaged = []
583
+ const stale = []
584
+
585
+ for (const [srcName, dstName] of MD_COPY_DIRS) {
586
+ const srcPath = path.join(sourceRoot, srcName)
587
+ const dstPath = path.join(targetRoot, dstName)
588
+
589
+ // source 扫描(经 shouldCopyMd 过滤)
590
+ const sourceFiles = new Set()
591
+ try {
592
+ const entries = fs.readdirSync(srcPath)
593
+ for (const base of entries) {
594
+ if (!shouldCopyMd(base)) continue
595
+ try { if (!fs.statSync(path.join(srcPath, base)).isFile()) continue } catch { continue }
596
+ sourceFiles.add(base)
597
+ }
598
+ } catch {
599
+ // 源目录不存在(发布包未含该目录)→ sourceFiles 为空,正常走后续逻辑
600
+ }
601
+
602
+ // manifest 登记列表
603
+ const manifestList = manifest[dstName] ?? []
604
+ const manifestSet = new Set(manifestList)
605
+
606
+ // disk 实际文件(目录不存在时视为空)
607
+ const diskSet = new Set()
608
+ try {
609
+ const diskEntries = fs.readdirSync(dstPath)
610
+ for (const base of diskEntries) {
611
+ try { if (!fs.statSync(path.join(dstPath, base)).isFile()) continue } catch { continue }
612
+ diskSet.add(base)
613
+ }
614
+ } catch {
615
+ // 目录不存在 → diskSet 为空
616
+ }
617
+
618
+ // D1:manifest 登记但 disk 无
619
+ for (const base of manifestList) {
620
+ if (!diskSet.has(base)) {
621
+ missing.push({ dir: dstName, file: base })
622
+ }
623
+ }
624
+
625
+ // D2:source 有但 disk 无(未安装或被删)
626
+ for (const base of sourceFiles) {
627
+ if (!diskSet.has(base)) {
628
+ unmanaged.push({ dir: dstName, file: base })
629
+ }
630
+ }
631
+
632
+ // D3:disk 有 & source 有 但 manifest 无(manifest 失配)
633
+ for (const base of diskSet) {
634
+ if (sourceFiles.has(base) && !manifestSet.has(base)) {
635
+ stale.push({ dir: dstName, file: base })
636
+ }
637
+ }
638
+ }
639
+
640
+ return { missing, unmanaged, stale }
641
+ }
642
+
554
643
  // 差集清理:prevList − sourceSet,仅删目标目录里确实存在的文件。返回清理计数。
555
644
  // 第三方文件(如 teamkit-git.md)从不写入 manifest → 不在 prevList → 永不被删。
556
645
  function pruneStale({ dstPath, prevList, sourceSet }) {
@@ -826,6 +915,8 @@ export {
826
915
  readMdManifest,
827
916
  writeMdManifest,
828
917
  MD_MANIFEST_REL,
918
+ compareManifestVsDisk,
919
+ MD_COPY_DIRS,
829
920
  }
830
921
 
831
922
  // main 守卫:仅作为脚本直接运行时执行
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,