@skillsmith/core 0.12.2 → 0.12.4

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 (285) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +2 -2
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/config/owned-lock.acquire.d.ts +8 -0
  5. package/dist/src/config/owned-lock.acquire.d.ts.map +1 -1
  6. package/dist/src/config/owned-lock.acquire.js +19 -4
  7. package/dist/src/config/owned-lock.acquire.js.map +1 -1
  8. package/dist/src/config/owned-lock.claim.d.ts +4 -2
  9. package/dist/src/config/owned-lock.claim.d.ts.map +1 -1
  10. package/dist/src/config/owned-lock.claim.js +42 -8
  11. package/dist/src/config/owned-lock.claim.js.map +1 -1
  12. package/dist/src/config/owned-lock.d.ts.map +1 -1
  13. package/dist/src/config/owned-lock.js +1 -0
  14. package/dist/src/config/owned-lock.js.map +1 -1
  15. package/dist/src/config/owned-lock.test.js +126 -2
  16. package/dist/src/config/owned-lock.test.js.map +1 -1
  17. package/dist/src/config/owned-lock.types.d.ts +9 -0
  18. package/dist/src/config/owned-lock.types.d.ts.map +1 -1
  19. package/dist/src/db/createDatabase.d.ts.map +1 -1
  20. package/dist/src/db/createDatabase.js +0 -2
  21. package/dist/src/db/createDatabase.js.map +1 -1
  22. package/dist/src/exports/services.install.d.ts +1 -0
  23. package/dist/src/exports/services.install.d.ts.map +1 -1
  24. package/dist/src/exports/services.install.js +5 -0
  25. package/dist/src/exports/services.install.js.map +1 -1
  26. package/dist/src/index.d.ts +2 -1
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +3 -1
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/src/install/fan-out.cleanup.d.ts +37 -0
  31. package/dist/src/install/fan-out.cleanup.d.ts.map +1 -0
  32. package/dist/src/install/fan-out.cleanup.js +75 -0
  33. package/dist/src/install/fan-out.cleanup.js.map +1 -0
  34. package/dist/src/install/fan-out.d.ts +41 -31
  35. package/dist/src/install/fan-out.d.ts.map +1 -1
  36. package/dist/src/install/fan-out.js +213 -112
  37. package/dist/src/install/fan-out.js.map +1 -1
  38. package/dist/src/install/fan-out.leftovers.d.ts +60 -0
  39. package/dist/src/install/fan-out.leftovers.d.ts.map +1 -0
  40. package/dist/src/install/fan-out.leftovers.js +251 -0
  41. package/dist/src/install/fan-out.leftovers.js.map +1 -0
  42. package/dist/src/install/fan-out.locks.d.ts +12 -0
  43. package/dist/src/install/fan-out.locks.d.ts.map +1 -0
  44. package/dist/src/install/fan-out.locks.js +114 -0
  45. package/dist/src/install/fan-out.locks.js.map +1 -0
  46. package/dist/src/install/fan-out.manifest.d.ts +64 -0
  47. package/dist/src/install/fan-out.manifest.d.ts.map +1 -0
  48. package/dist/src/install/fan-out.manifest.js +130 -0
  49. package/dist/src/install/fan-out.manifest.js.map +1 -0
  50. package/dist/src/install/fan-out.overwrite.d.ts +51 -0
  51. package/dist/src/install/fan-out.overwrite.d.ts.map +1 -0
  52. package/dist/src/install/fan-out.overwrite.js +343 -0
  53. package/dist/src/install/fan-out.overwrite.js.map +1 -0
  54. package/dist/src/install/remove-if-same.d.ts +58 -0
  55. package/dist/src/install/remove-if-same.d.ts.map +1 -0
  56. package/dist/src/install/remove-if-same.js +222 -0
  57. package/dist/src/install/remove-if-same.js.map +1 -0
  58. package/dist/src/provenance/backfill.d.ts.map +1 -1
  59. package/dist/src/provenance/backfill.js +15 -2
  60. package/dist/src/provenance/backfill.js.map +1 -1
  61. package/dist/src/scripts/__tests__/scan-imported-skills.test.js +9 -1
  62. package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
  63. package/dist/src/scripts/scan-imported-skills.d.ts +7 -1
  64. package/dist/src/scripts/scan-imported-skills.d.ts.map +1 -1
  65. package/dist/src/scripts/scan-imported-skills.js +15 -2
  66. package/dist/src/scripts/scan-imported-skills.js.map +1 -1
  67. package/dist/src/scripts/skill-scanner/allowlist.d.ts +10 -0
  68. package/dist/src/scripts/skill-scanner/allowlist.d.ts.map +1 -1
  69. package/dist/src/scripts/skill-scanner/allowlist.js +10 -0
  70. package/dist/src/scripts/skill-scanner/allowlist.js.map +1 -1
  71. package/dist/src/scripts/skill-scanner/index.d.ts +8 -0
  72. package/dist/src/scripts/skill-scanner/index.d.ts.map +1 -1
  73. package/dist/src/scripts/skill-scanner/index.js +13 -5
  74. package/dist/src/scripts/skill-scanner/index.js.map +1 -1
  75. package/dist/src/scripts/skill-scanner/trust-scorer.d.ts.map +1 -1
  76. package/dist/src/scripts/skill-scanner/trust-scorer.js +6 -0
  77. package/dist/src/scripts/skill-scanner/trust-scorer.js.map +1 -1
  78. package/dist/src/security/scanner/SecurityScanner.action-context.d.ts +15 -0
  79. package/dist/src/security/scanner/SecurityScanner.action-context.d.ts.map +1 -0
  80. package/dist/src/security/scanner/SecurityScanner.action-context.js +209 -0
  81. package/dist/src/security/scanner/SecurityScanner.action-context.js.map +1 -0
  82. package/dist/src/security/scanner/SecurityScanner.d.ts +25 -0
  83. package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
  84. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts +40 -1
  85. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
  86. package/dist/src/security/scanner/SecurityScanner.helpers.js +88 -51
  87. package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
  88. package/dist/src/security/scanner/SecurityScanner.js +39 -6
  89. package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
  90. package/dist/src/security/scanner/SecurityScanner.pii.d.ts +30 -0
  91. package/dist/src/security/scanner/SecurityScanner.pii.d.ts.map +1 -1
  92. package/dist/src/security/scanner/SecurityScanner.pii.js +8 -1
  93. package/dist/src/security/scanner/SecurityScanner.pii.js.map +1 -1
  94. package/dist/src/security/scanner/SecurityScanner.prose-lexicon.d.ts +60 -0
  95. package/dist/src/security/scanner/SecurityScanner.prose-lexicon.d.ts.map +1 -0
  96. package/dist/src/security/scanner/SecurityScanner.prose-lexicon.js +228 -0
  97. package/dist/src/security/scanner/SecurityScanner.prose-lexicon.js.map +1 -0
  98. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
  99. package/dist/src/security/scanner/SecurityScanner.scanners.js +42 -5
  100. package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
  101. package/dist/src/security/scanner/SecurityScanner.value-gate.d.ts +69 -0
  102. package/dist/src/security/scanner/SecurityScanner.value-gate.d.ts.map +1 -0
  103. package/dist/src/security/scanner/SecurityScanner.value-gate.js +271 -0
  104. package/dist/src/security/scanner/SecurityScanner.value-gate.js.map +1 -0
  105. package/dist/src/security/scanner/SecurityScanner.weak-passwords.d.ts +55 -0
  106. package/dist/src/security/scanner/SecurityScanner.weak-passwords.d.ts.map +1 -0
  107. package/dist/src/security/scanner/SecurityScanner.weak-passwords.js +308 -0
  108. package/dist/src/security/scanner/SecurityScanner.weak-passwords.js.map +1 -0
  109. package/dist/src/security/scanner/patterns.d.ts +61 -6
  110. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  111. package/dist/src/security/scanner/patterns.js +70 -61
  112. package/dist/src/security/scanner/patterns.js.map +1 -1
  113. package/dist/src/security/scanner/patterns.sensitive-path.d.ts +73 -0
  114. package/dist/src/security/scanner/patterns.sensitive-path.d.ts.map +1 -0
  115. package/dist/src/security/scanner/patterns.sensitive-path.js +192 -0
  116. package/dist/src/security/scanner/patterns.sensitive-path.js.map +1 -0
  117. package/dist/src/security/scanner/regex-utils.d.ts +31 -17
  118. package/dist/src/security/scanner/regex-utils.d.ts.map +1 -1
  119. package/dist/src/security/scanner/regex-utils.js +31 -17
  120. package/dist/src/security/scanner/regex-utils.js.map +1 -1
  121. package/dist/src/security/scanner/types.d.ts +9 -0
  122. package/dist/src/security/scanner/types.d.ts.map +1 -1
  123. package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -1
  124. package/dist/src/services/agent-pack/prompt-source.js +3 -2
  125. package/dist/src/services/agent-pack/prompt-source.js.map +1 -1
  126. package/dist/src/services/skill-installation.content.d.ts +7 -2
  127. package/dist/src/services/skill-installation.content.d.ts.map +1 -1
  128. package/dist/src/services/skill-installation.content.js +75 -28
  129. package/dist/src/services/skill-installation.content.js.map +1 -1
  130. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  131. package/dist/src/services/skill-installation.helpers.js +8 -0
  132. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  133. package/dist/src/services/skill-installation.io.d.ts +1 -29
  134. package/dist/src/services/skill-installation.io.d.ts.map +1 -1
  135. package/dist/src/services/skill-installation.io.dirs.d.ts +15 -0
  136. package/dist/src/services/skill-installation.io.dirs.d.ts.map +1 -0
  137. package/dist/src/services/skill-installation.io.dirs.js +84 -0
  138. package/dist/src/services/skill-installation.io.dirs.js.map +1 -0
  139. package/dist/src/services/skill-installation.io.js +6 -176
  140. package/dist/src/services/skill-installation.io.js.map +1 -1
  141. package/dist/src/services/skill-installation.io.rollback.d.ts +138 -0
  142. package/dist/src/services/skill-installation.io.rollback.d.ts.map +1 -0
  143. package/dist/src/services/skill-installation.io.rollback.js +218 -0
  144. package/dist/src/services/skill-installation.io.rollback.js.map +1 -0
  145. package/dist/src/services/skill-installation.io.write.d.ts +69 -0
  146. package/dist/src/services/skill-installation.io.write.d.ts.map +1 -0
  147. package/dist/src/services/skill-installation.io.write.js +417 -0
  148. package/dist/src/services/skill-installation.io.write.js.map +1 -0
  149. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  150. package/dist/src/services/skill-installation.service.finalize.d.ts +62 -0
  151. package/dist/src/services/skill-installation.service.finalize.d.ts.map +1 -0
  152. package/dist/src/services/skill-installation.service.finalize.js +136 -0
  153. package/dist/src/services/skill-installation.service.finalize.js.map +1 -0
  154. package/dist/src/services/skill-installation.service.js +66 -80
  155. package/dist/src/services/skill-installation.service.js.map +1 -1
  156. package/dist/src/services/skill-installation.target-guard.d.ts +82 -0
  157. package/dist/src/services/skill-installation.target-guard.d.ts.map +1 -0
  158. package/dist/src/services/skill-installation.target-guard.js +410 -0
  159. package/dist/src/services/skill-installation.target-guard.js.map +1 -0
  160. package/dist/src/services/skill-installation.types.d.ts +13 -1
  161. package/dist/src/services/skill-installation.types.d.ts.map +1 -1
  162. package/dist/src/services/skill-installation.types.js.map +1 -1
  163. package/dist/src/services/skill-installation.uninstall.d.ts +3 -1
  164. package/dist/src/services/skill-installation.uninstall.d.ts.map +1 -1
  165. package/dist/src/services/skill-installation.uninstall.helpers.d.ts +59 -0
  166. package/dist/src/services/skill-installation.uninstall.helpers.d.ts.map +1 -0
  167. package/dist/src/services/skill-installation.uninstall.helpers.js +124 -0
  168. package/dist/src/services/skill-installation.uninstall.helpers.js.map +1 -0
  169. package/dist/src/services/skill-installation.uninstall.js +148 -25
  170. package/dist/src/services/skill-installation.uninstall.js.map +1 -1
  171. package/dist/src/utils/index.d.ts +1 -0
  172. package/dist/src/utils/index.d.ts.map +1 -1
  173. package/dist/src/utils/index.js +2 -0
  174. package/dist/src/utils/index.js.map +1 -1
  175. package/dist/src/utils/safe-fs.d.ts +69 -0
  176. package/dist/src/utils/safe-fs.d.ts.map +1 -1
  177. package/dist/src/utils/safe-fs.js +113 -2
  178. package/dist/src/utils/safe-fs.js.map +1 -1
  179. package/dist/src/utils/skill-name.d.ts +19 -0
  180. package/dist/src/utils/skill-name.d.ts.map +1 -0
  181. package/dist/src/utils/skill-name.js +26 -0
  182. package/dist/src/utils/skill-name.js.map +1 -0
  183. package/dist/src/utils/skill-name.test.d.ts +2 -0
  184. package/dist/src/utils/skill-name.test.d.ts.map +1 -0
  185. package/dist/src/utils/skill-name.test.js +34 -0
  186. package/dist/src/utils/skill-name.test.js.map +1 -0
  187. package/dist/tests/install/fan-out.test.js +1951 -13
  188. package/dist/tests/install/fan-out.test.js.map +1 -1
  189. package/dist/tests/install/remove-if-same.test.d.ts +2 -0
  190. package/dist/tests/install/remove-if-same.test.d.ts.map +1 -0
  191. package/dist/tests/install/remove-if-same.test.js +298 -0
  192. package/dist/tests/install/remove-if-same.test.js.map +1 -0
  193. package/dist/tests/integration/fan-out-lock-contention.test.d.ts +11 -0
  194. package/dist/tests/integration/fan-out-lock-contention.test.d.ts.map +1 -0
  195. package/dist/tests/integration/fan-out-lock-contention.test.js +53 -0
  196. package/dist/tests/integration/fan-out-lock-contention.test.js.map +1 -0
  197. package/dist/tests/provenance/backfill.test.js +43 -0
  198. package/dist/tests/provenance/backfill.test.js.map +1 -1
  199. package/dist/tests/security/ContinuousSecurity.performance.test.js +12 -7
  200. package/dist/tests/security/ContinuousSecurity.performance.test.js.map +1 -1
  201. package/dist/tests/security/ContinuousSecurity.test.js +14 -2
  202. package/dist/tests/security/ContinuousSecurity.test.js.map +1 -1
  203. package/dist/tests/security/ai-defence-multiline-promotion.test.d.ts +10 -12
  204. package/dist/tests/security/ai-defence-multiline-promotion.test.d.ts.map +1 -1
  205. package/dist/tests/security/ai-defence-multiline-promotion.test.js +26 -33
  206. package/dist/tests/security/ai-defence-multiline-promotion.test.js.map +1 -1
  207. package/dist/tests/security/nested-instruction-long-attribute.test.js +20 -10
  208. package/dist/tests/security/nested-instruction-long-attribute.test.js.map +1 -1
  209. package/dist/tests/security/scanner-evidence-tiers.test.js +15 -0
  210. package/dist/tests/security/scanner-evidence-tiers.test.js.map +1 -1
  211. package/dist/tests/security/scanner-multiline-cap.test.d.ts +21 -0
  212. package/dist/tests/security/scanner-multiline-cap.test.d.ts.map +1 -0
  213. package/dist/tests/security/scanner-multiline-cap.test.js +152 -0
  214. package/dist/tests/security/scanner-multiline-cap.test.js.map +1 -0
  215. package/dist/tests/security/scanner-regression-guard.test.js +66 -1
  216. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  217. package/dist/tests/security/scanner-witness-sw1.test.d.ts +32 -0
  218. package/dist/tests/security/scanner-witness-sw1.test.d.ts.map +1 -0
  219. package/dist/tests/security/scanner-witness-sw1.test.js +255 -0
  220. package/dist/tests/security/scanner-witness-sw1.test.js.map +1 -0
  221. package/dist/tests/security/sensitive-path-adversarial-review.test.d.ts +2 -0
  222. package/dist/tests/security/sensitive-path-adversarial-review.test.d.ts.map +1 -0
  223. package/dist/tests/security/sensitive-path-adversarial-review.test.js +313 -0
  224. package/dist/tests/security/sensitive-path-adversarial-review.test.js.map +1 -0
  225. package/dist/tests/security/sensitive-path-boolean-flag.test.d.ts +2 -0
  226. package/dist/tests/security/sensitive-path-boolean-flag.test.d.ts.map +1 -0
  227. package/dist/tests/security/sensitive-path-boolean-flag.test.js +151 -0
  228. package/dist/tests/security/sensitive-path-boolean-flag.test.js.map +1 -0
  229. package/dist/tests/security/sensitive-path-fp.test.js +314 -0
  230. package/dist/tests/security/sensitive-path-fp.test.js.map +1 -1
  231. package/dist/tests/security/sensitive-path-prefixed-secrets.test.d.ts +2 -0
  232. package/dist/tests/security/sensitive-path-prefixed-secrets.test.d.ts.map +1 -0
  233. package/dist/tests/security/sensitive-path-prefixed-secrets.test.js +176 -0
  234. package/dist/tests/security/sensitive-path-prefixed-secrets.test.js.map +1 -0
  235. package/dist/tests/security/smi5879-corroboration.core.test.js +44 -0
  236. package/dist/tests/security/smi5879-corroboration.core.test.js.map +1 -1
  237. package/dist/tests/security/weak-password-lexicon.test.d.ts +18 -0
  238. package/dist/tests/security/weak-password-lexicon.test.d.ts.map +1 -0
  239. package/dist/tests/security/weak-password-lexicon.test.js +132 -0
  240. package/dist/tests/security/weak-password-lexicon.test.js.map +1 -0
  241. package/dist/tests/skill-scanner/allowlist.test.js +58 -4
  242. package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
  243. package/dist/tests/skill-scanner/categorizer.test.d.ts +2 -0
  244. package/dist/tests/skill-scanner/categorizer.test.d.ts.map +1 -0
  245. package/dist/tests/skill-scanner/categorizer.test.js +71 -0
  246. package/dist/tests/skill-scanner/categorizer.test.js.map +1 -0
  247. package/dist/tests/skill-scanner/cli-guard.test.d.ts +23 -0
  248. package/dist/tests/skill-scanner/cli-guard.test.d.ts.map +1 -0
  249. package/dist/tests/skill-scanner/cli-guard.test.js +71 -0
  250. package/dist/tests/skill-scanner/cli-guard.test.js.map +1 -0
  251. package/dist/tests/unit/services/skill-installation.content.test.js +59 -0
  252. package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -1
  253. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +35 -14
  254. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -1
  255. package/dist/tests/unit/services/skill-installation.io.rollback.test.d.ts +2 -0
  256. package/dist/tests/unit/services/skill-installation.io.rollback.test.d.ts.map +1 -0
  257. package/dist/tests/unit/services/skill-installation.io.rollback.test.js +984 -0
  258. package/dist/tests/unit/services/skill-installation.io.rollback.test.js.map +1 -0
  259. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.d.ts +2 -0
  260. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.d.ts.map +1 -0
  261. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.js +148 -0
  262. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.js.map +1 -0
  263. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +19 -0
  264. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -1
  265. package/dist/tests/unit/services/skill-installation.service.finalize.test.d.ts +2 -0
  266. package/dist/tests/unit/services/skill-installation.service.finalize.test.d.ts.map +1 -0
  267. package/dist/tests/unit/services/skill-installation.service.finalize.test.js +147 -0
  268. package/dist/tests/unit/services/skill-installation.service.finalize.test.js.map +1 -0
  269. package/dist/tests/unit/services/skill-installation.service.sensitive-path-fp.test.d.ts +29 -0
  270. package/dist/tests/unit/services/skill-installation.service.sensitive-path-fp.test.d.ts.map +1 -0
  271. package/dist/tests/unit/services/skill-installation.service.sensitive-path-fp.test.js +200 -0
  272. package/dist/tests/unit/services/skill-installation.service.sensitive-path-fp.test.js.map +1 -0
  273. package/dist/tests/unit/services/skill-installation.service.test.js +43 -0
  274. package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
  275. package/dist/tests/unit/services/skill-installation.target-guard.test.d.ts +2 -0
  276. package/dist/tests/unit/services/skill-installation.target-guard.test.d.ts.map +1 -0
  277. package/dist/tests/unit/services/skill-installation.target-guard.test.js +590 -0
  278. package/dist/tests/unit/services/skill-installation.target-guard.test.js.map +1 -0
  279. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.d.ts +2 -0
  280. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.d.ts.map +1 -0
  281. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.js +473 -0
  282. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.js.map +1 -0
  283. package/dist/tests/utils/safe-fs.test.js +129 -2
  284. package/dist/tests/utils/safe-fs.test.js.map +1 -1
  285. package/package.json +6 -1
@@ -0,0 +1,251 @@
1
+ /**
2
+ * SMI-6529: what a crashed or interrupted fan-out refresh leaves next to a
3
+ * destination, and what may be done about it — the hidden backup, staging and
4
+ * parked folders, the one case where a stranded backup is restored, and the
5
+ * warnings the user sees. Split out of fan-out.overwrite.ts in round 16 to
6
+ * stay under the 500-line gate; it holds the naming helpers both halves share.
7
+ *
8
+ * Nothing here deletes anything with content: `rmdir` refuses a folder that
9
+ * holds anything, and neither a folder's name nor its contents proves who owns
10
+ * it now (round 14), so everything else is reported and left alone.
11
+ *
12
+ * @module @skillsmith/core/install/fan-out.leftovers
13
+ */
14
+ import * as path from 'node:path';
15
+ import * as fsp from 'node:fs/promises';
16
+ import { PARK_TAG, parkedPattern } from './remove-if-same.js';
17
+ function errorCode(err) {
18
+ const code = err?.code;
19
+ return code ?? (err instanceof Error ? err.message : String(err));
20
+ }
21
+ /**
22
+ * One message for entries a scan could not inspect. Round 25 (cross-model
23
+ * review): the parent listing succeeding does not mean every entry under it
24
+ * could be read, and an entry that could not be read is not an entry that
25
+ * isn't there.
26
+ */
27
+ function unreadableNote(items, dest) {
28
+ if (items.length === 0)
29
+ return undefined;
30
+ return (`${items.join('; ')}, so anything an interrupted refresh or removal left beside ${dest} ` +
31
+ `may not be fully reported.`);
32
+ }
33
+ export const BACKUP_TAG = '.skillsmith-backup-';
34
+ export const STAGING_TAG = '.skillsmith-staging-';
35
+ export function escapeRegExp(s) {
36
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
+ }
38
+ /** Hidden sibling name prefix for `dest`, e.g. `.foo.skillsmith-backup-`. */
39
+ export function siblingPrefix(dest, tag) {
40
+ return '.' + path.basename(dest) + tag;
41
+ }
42
+ /** Exact match for a `mkdtemp` folder made from `siblingPrefix(dest, tag)` (6-char suffix). */
43
+ export function siblingPattern(dest, tag) {
44
+ return new RegExp('^' + escapeRegExp(siblingPrefix(dest, tag)) + '[A-Za-z0-9]{6}$');
45
+ }
46
+ /**
47
+ * Names `removeIfSame` parks `dest`, or one of its backup or staging folders,
48
+ * under while removing it (`.<name>.skillsmith-removing-<32 hex>`). Exact, so
49
+ * a sibling skill's names (`foo.bar` for `foo`) never match.
50
+ */
51
+ function parkedPatterns(dest) {
52
+ const park = escapeRegExp(PARK_TAG) + '[0-9a-f]{32}$';
53
+ return [
54
+ parkedPattern(dest),
55
+ ...[BACKUP_TAG, STAGING_TAG].map((tag) => new RegExp('^\\.' + escapeRegExp(siblingPrefix(dest, tag)) + '[A-Za-z0-9]{6}' + park)),
56
+ ];
57
+ }
58
+ export async function lstatOrNull(p) {
59
+ try {
60
+ return await fsp.lstat(p);
61
+ }
62
+ catch (err) {
63
+ if (err.code === 'ENOENT')
64
+ return null;
65
+ throw err;
66
+ }
67
+ }
68
+ /** Whether the link manifest records `dest` as a copy Skillsmith made. */
69
+ export function isRecordedCopy(dest, manifest) {
70
+ const resolved = path.resolve(dest);
71
+ return manifest.links.some((link) => link.kind === 'copy' && path.resolve(link.to) === resolved);
72
+ }
73
+ /**
74
+ * Remove `folder` if it is empty, and say whether it was. `rmdir` refuses a
75
+ * folder with anything in it, so this can never delete content. Call only
76
+ * under the destination lock: our own backup folder is briefly empty mid-swap.
77
+ */
78
+ async function removeIfEmpty(folder) {
79
+ try {
80
+ await fsp.rmdir(folder);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ /**
88
+ * Housekeeping under the destination lock, before any write:
89
+ * - remove an empty backup folder (a crash before anything was moved in);
90
+ * - restore a backup stranded by a crash between the rename-aside and the
91
+ * swap: the destination is missing, this is the only backup whose
92
+ * `original` is a real directory, and the manifest still records the
93
+ * destination as a copy. Backups are only ever made of recorded copies,
94
+ * so an unrecorded one has been uninstalled since, and restoring it would
95
+ * bring back a stale, untracked copy. With two or more, nothing says which
96
+ * is newest (a name can sort anywhere), so none is restored (round 9).
97
+ *
98
+ * Any other backup is left in place for `listLeftoverBackups` to report, and
99
+ * so is a staging folder left by a crashed write: round 14 (cross-model
100
+ * review) stopped deleting those here, since neither a folder's name nor its
101
+ * contents proves who owns it now. Returns the backup folders restored from.
102
+ */
103
+ export async function recoverDestination(dest, manifest) {
104
+ const parent = path.dirname(dest);
105
+ let entries;
106
+ try {
107
+ entries = await fsp.readdir(parent);
108
+ }
109
+ catch (err) {
110
+ return {
111
+ restored: [],
112
+ unreadable: `${parent} could not be listed (${errorCode(err)}), so anything an interrupted refresh ` +
113
+ `left beside ${dest} was neither recovered nor reported.`,
114
+ };
115
+ }
116
+ const backup = siblingPattern(dest, BACKUP_TAG);
117
+ const candidates = [];
118
+ const unreadable = [];
119
+ for (const name of entries) {
120
+ const folder = path.join(parent, name);
121
+ if (!backup.test(name))
122
+ continue;
123
+ if (await removeIfEmpty(folder))
124
+ continue;
125
+ let originalStat;
126
+ try {
127
+ originalStat = await lstatOrNull(path.join(folder, 'original'));
128
+ }
129
+ catch (err) {
130
+ // Round 25 (cross-model review): this swallowed the failure and treated
131
+ // the folder as "not a candidate", so a backup that could not be read
132
+ // was silently skipped instead of reported.
133
+ unreadable.push(`${folder} could not be inspected (${errorCode(err)})`);
134
+ continue;
135
+ }
136
+ if (originalStat?.isDirectory())
137
+ candidates.push(folder);
138
+ }
139
+ const note = unreadableNote(unreadable, dest);
140
+ const folder = candidates.length === 1 ? candidates[0] : undefined;
141
+ if (folder === undefined || !isRecordedCopy(dest, manifest)) {
142
+ return { restored: [], unreadable: note };
143
+ }
144
+ // Round 19 (Opus): this check sits immediately before the rename below,
145
+ // which is as narrow as Node allows — there is no atomic no-clobber rename
146
+ // for a directory — so an entry created inside that window would be
147
+ // replaced (measured: an empty directory). Not restoring at all would lose
148
+ // the crash recovery this exists for, so the window is accepted and stated.
149
+ if ((await lstatOrNull(dest)) !== null)
150
+ return { restored: [], unreadable: note };
151
+ await fsp.rename(path.join(folder, 'original'), dest);
152
+ await fsp.rmdir(folder).catch(() => { });
153
+ return { restored: [folder], unreadable: note };
154
+ }
155
+ /**
156
+ * Backup folders for `dest` that are still present: copies an interrupted
157
+ * refresh left behind (after the swap, or unrestorable). Round 7: these are
158
+ * reported to the user rather than kept silently or deleted. Round 8: an
159
+ * empty one holds nothing to report and is removed. Round 9: on a volume
160
+ * that ignores case, a backup made under another spelling of the name
161
+ * (`.Foo.` for `foo`) belongs to this destination too, so it is reported;
162
+ * it is never removed or restored, since on a case-sensitive volume the
163
+ * same name belongs to a different destination. Round 14: a staging folder
164
+ * left by a crashed write is reported the same way. Round 15: so is what a
165
+ * crashed or failed removal left under a parked name. Call under the lock.
166
+ */
167
+ export async function listLeftoverBackups(dest) {
168
+ const parent = path.dirname(dest);
169
+ let entries;
170
+ try {
171
+ entries = await fsp.readdir(parent);
172
+ }
173
+ catch (err) {
174
+ return {
175
+ folders: [],
176
+ unreadable: `${parent} could not be listed (${errorCode(err)}), so anything an interrupted refresh ` +
177
+ `or removal left beside ${dest} is not reported here.`,
178
+ };
179
+ }
180
+ const exact = siblingPattern(dest, BACKUP_TAG);
181
+ const staging = siblingPattern(dest, STAGING_TAG);
182
+ const parked = parkedPatterns(dest);
183
+ const anyCase = new RegExp(exact.source, 'i');
184
+ const leftovers = [];
185
+ const unreadable = [];
186
+ for (const name of entries) {
187
+ const folder = path.join(parent, name);
188
+ if (exact.test(name) || staging.test(name) || parked.some((p) => p.test(name))) {
189
+ if (!(await removeIfEmpty(folder)))
190
+ leftovers.push(folder);
191
+ }
192
+ else if (anyCase.test(name)) {
193
+ const variant = await isCaseVariantOf(folder, dest);
194
+ if ('unreadable' in variant)
195
+ unreadable.push(variant.unreadable);
196
+ else if (variant.variant)
197
+ leftovers.push(folder);
198
+ }
199
+ }
200
+ return { folders: leftovers, unreadable: unreadableNote(unreadable, dest) };
201
+ }
202
+ /**
203
+ * Whether backup `folder` (named for another spelling of `dest`) is, on this
204
+ * volume, the same entry as the name spelled for `dest`, and holds content.
205
+ * On a case-sensitive volume that name doesn't exist, or is a different entry.
206
+ */
207
+ async function isCaseVariantOf(folder, dest) {
208
+ const suffix = path.basename(folder).slice(-6);
209
+ const ownName = path.join(path.dirname(dest), siblingPrefix(dest, BACKUP_TAG) + suffix);
210
+ // Round 25 (cross-model review): both of these swallowed a failed inspection
211
+ // and answered "not a variant", and the read below answered "empty", so a
212
+ // folder that could not be read was reported as nothing at all.
213
+ let a;
214
+ let b;
215
+ try {
216
+ ;
217
+ [a, b] = await Promise.all([lstatOrNull(folder), lstatOrNull(ownName)]);
218
+ }
219
+ catch (err) {
220
+ return { unreadable: `${folder} could not be inspected (${errorCode(err)})` };
221
+ }
222
+ if (a === null || b === null || a.dev !== b.dev || a.ino !== b.ino)
223
+ return { variant: false };
224
+ let contents;
225
+ try {
226
+ contents = await fsp.readdir(folder);
227
+ }
228
+ catch (err) {
229
+ return { unreadable: `${folder} could not be listed (${errorCode(err)})` };
230
+ }
231
+ return { variant: contents.length > 0 };
232
+ }
233
+ /** User-facing warning for a leftover backup, staging folder or parked entry. */
234
+ export function leftoverBackupWarning(folder) {
235
+ const name = path.basename(folder);
236
+ if (name.includes(PARK_TAG)) {
237
+ // Round 18: who owns a parked entry is not known — see parkedLeftoverWarning.
238
+ return (`an interrupted removal moved something aside to the hidden path ${folder} and did not ` +
239
+ `finish. It may be part of this skill's copy, or something another program put there. ` +
240
+ `Check it, then restore or delete it yourself.`);
241
+ }
242
+ // Round 23 (Opus): this used to call the contents "an earlier copy" of the
243
+ // skill. That is an ownership claim it cannot make — the move-aside window
244
+ // can displace another program's entry into this folder — and it is the same
245
+ // claim the parked wording above dropped.
246
+ const what = name.includes(STAGING_TAG) ? 'a partial copy' : 'a copy';
247
+ return (`an interrupted refresh left ${what} in the hidden folder ${folder}. It is either what this ` +
248
+ `skill replaced, or something another program put at the path while the refresh was moving ` +
249
+ `the old copy aside. Check it, then restore or delete it yourself.`);
250
+ }
251
+ //# sourceMappingURL=fan-out.leftovers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.leftovers.js","sourceRoot":"","sources":["../../../src/install/fan-out.leftovers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Run `fn` holding the cross-process file lock `<target>.lock`, waiting
3
+ * without blocking the event loop. Callers in one process contend on it too.
4
+ */
5
+ export declare function withFileLock<T>(target: string, label: string, fn: () => Promise<T>): Promise<T>;
6
+ /**
7
+ * Run `fn` holding the lock for `dest`: first queue behind any other call
8
+ * for the same destination in this process, then take the cross-process
9
+ * file lock `.<name>.skillsmith-fanout.lock` next to it.
10
+ */
11
+ export declare function withDestinationLock<T>(dest: string, fn: () => Promise<T>): Promise<T>;
12
+ //# sourceMappingURL=fan-out.locks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.locks.d.ts","sourceRoot":"","sources":["../../../src/install/fan-out.locks.ts"],"names":[],"mappings":"AA4EA;;;GAGG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAmB3F"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @fileoverview The per-destination locking used by every fan-out write: an
3
+ * in-process queue so same-process callers serialize, and a cross-process
4
+ * file lock taken without ever blocking the event loop.
5
+ * @module @skillsmith/core/install/fan-out.locks
6
+ *
7
+ * Split out of `fan-out.overwrite.ts` in SMI-6529 round 27, when that file
8
+ * reached 505 lines against the 500-line standard — the same sibling-split
9
+ * convention `fan-out.leftovers.ts` already follows. `fan-out.overwrite.ts`
10
+ * re-exports both entry points, so existing importers are unchanged.
11
+ */
12
+ import * as path from 'node:path';
13
+ import * as fsp from 'node:fs/promises';
14
+ import { setTimeout as delay } from 'node:timers/promises';
15
+ import { acquireOwnedLock, StuckLockError } from '../config/owned-lock.js';
16
+ import { siblingPrefix } from './fan-out.leftovers.js';
17
+ const LOCK_TAG = '.skillsmith-fanout';
18
+ /** How long to wait for another process's lock on the same destination (ms). */
19
+ const DESTINATION_LOCK_TIMEOUT_MS = 30_000;
20
+ /** Pause between attempts while another process holds the lock (ms). */
21
+ const DESTINATION_LOCK_POLL_MS = 50;
22
+ /**
23
+ * Refusals that end on their own, so they are waited out: a live holder, a
24
+ * busy reclaim lock, and a holder we may not reclaim because auto-reclaim is
25
+ * off (it still ends when that holder releases). An unparseable or legacy
26
+ * claim never goes away by itself, so it fails at once.
27
+ */
28
+ const RETRYABLE_REASONS = new Set([
29
+ 'held',
30
+ 'reclaim_unavailable',
31
+ 'reclaim_disabled',
32
+ ]);
33
+ // Same-process callers for one destination queue here, so only one of them
34
+ // holds the file lock at a time; the file lock arbitrates between processes.
35
+ const inProcessQueue = new Map();
36
+ /**
37
+ * Queue key for `dest`: its parent's real path plus its name, so a symlinked
38
+ * spelling of the same folder shares one queue (round 7). A case-only variant
39
+ * on a case-insensitive volume still gets its own key, but it then only waits
40
+ * asynchronously on the shared file lock below, never blocking.
41
+ */
42
+ async function queueKey(dest) {
43
+ const parent = path.dirname(path.resolve(dest));
44
+ const realParent = await fsp.realpath(parent).catch(() => parent);
45
+ return path.join(realParent, path.basename(dest));
46
+ }
47
+ /**
48
+ * Take the cross-process file lock without ever blocking the event loop.
49
+ * `acquireOwnedLock` normally waits with a synchronous sleep, which froze a
50
+ * whole MCP server for up to 10 s (round 7). Here each attempt is one
51
+ * non-waiting try (`timeoutMs: 0`) that still reclaims a dead holder's lock
52
+ * at once (`reclaimProbeAfterMs: 0`) and never waits on the reclaim lock
53
+ * either (`reclaimLockTimeoutMs: 0`, round 8). Between attempts we await.
54
+ */
55
+ async function acquireFileLock(target, label) {
56
+ const deadline = Date.now() + DESTINATION_LOCK_TIMEOUT_MS;
57
+ for (;;) {
58
+ try {
59
+ return acquireOwnedLock(target, {
60
+ timeoutMs: 0,
61
+ reclaimProbeAfterMs: 0,
62
+ reclaimLockTimeoutMs: 0,
63
+ label,
64
+ });
65
+ }
66
+ catch (err) {
67
+ const retryable = err instanceof StuckLockError && RETRYABLE_REASONS.has(err.reason);
68
+ if (!retryable || Date.now() >= deadline)
69
+ throw err;
70
+ await delay(DESTINATION_LOCK_POLL_MS);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Run `fn` holding the cross-process file lock `<target>.lock`, waiting
76
+ * without blocking the event loop. Callers in one process contend on it too.
77
+ */
78
+ export async function withFileLock(target, label, fn) {
79
+ const release = await acquireFileLock(target, label);
80
+ try {
81
+ return await fn();
82
+ }
83
+ finally {
84
+ release();
85
+ }
86
+ }
87
+ /**
88
+ * Run `fn` holding the lock for `dest`: first queue behind any other call
89
+ * for the same destination in this process, then take the cross-process
90
+ * file lock `.<name>.skillsmith-fanout.lock` next to it.
91
+ */
92
+ export async function withDestinationLock(dest, fn) {
93
+ const key = await queueKey(dest);
94
+ const previous = inProcessQueue.get(key) ?? Promise.resolve();
95
+ let releaseQueue = () => { };
96
+ const mine = new Promise((resolve) => {
97
+ releaseQueue = resolve;
98
+ });
99
+ const tail = previous.then(() => mine);
100
+ inProcessQueue.set(key, tail);
101
+ await previous;
102
+ try {
103
+ const parent = path.dirname(dest);
104
+ await fsp.mkdir(parent, { recursive: true });
105
+ const lockTarget = path.join(parent, siblingPrefix(dest, LOCK_TAG));
106
+ return await withFileLock(lockTarget, 'fan-out destination lock', fn);
107
+ }
108
+ finally {
109
+ releaseQueue();
110
+ if (inProcessQueue.get(key) === tail)
111
+ inProcessQueue.delete(key);
112
+ }
113
+ }
114
+ //# sourceMappingURL=fan-out.locks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.locks.js","sourceRoot":"","sources":["../../../src/install/fan-out.locks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /** How a fan-out destination was made: a relative symlink, or a recursive copy. */
2
+ export type LinkKind = 'symlink' | 'copy';
3
+ /** One fan-out Skillsmith made: which skill, from where, to where, how, and when. */
4
+ export interface LinkRecord {
5
+ skillId: string;
6
+ from: string;
7
+ to: string;
8
+ kind: LinkKind;
9
+ createdAt: string;
10
+ }
11
+ /** The on-disk link manifest: every fan-out Skillsmith has recorded. */
12
+ export interface LinkManifest {
13
+ version: 1;
14
+ links: LinkRecord[];
15
+ }
16
+ /**
17
+ * Resolve the manifest file path. Sits under `~/.skillsmith/` so it shares
18
+ * the existing allow-list entry for that directory (DEFAULT_ALLOWED_DIRS in
19
+ * `pathValidation.ts`).
20
+ */
21
+ export declare function getLinkManifestPath(): string;
22
+ /** What reading the manifest found. `unreadable` means it exists but must not be written over. */
23
+ export interface ManifestRead {
24
+ state: 'ok' | 'missing' | 'corrupt' | 'unreadable';
25
+ manifest: LinkManifest;
26
+ reason?: string;
27
+ }
28
+ /**
29
+ * Read the manifest and report its state. Valid JSON with a higher version is
30
+ * `unreadable`, not `corrupt`: a newer Skillsmith wrote it, and moving it
31
+ * aside would drop its records too (SMI-6529 round 10).
32
+ */
33
+ export declare function readManifestFile(): Promise<ManifestRead>;
34
+ /** Error for a manifest that exists but can't be read; nothing is written over it. */
35
+ export declare function unreadableManifestError(reason: string | undefined): Error;
36
+ /**
37
+ * Load the manifest for reading. Returns an empty manifest if the file is
38
+ * missing, unreadable or corrupt — fan-out is best-effort, never crash on
39
+ * cold start. Changes go through {@link updateManifest}, which never
40
+ * overwrites a file it couldn't read.
41
+ */
42
+ export declare function loadManifest(): Promise<LinkManifest>;
43
+ /**
44
+ * Apply `change` to the manifest and save it, under the manifest's own lock.
45
+ * Returns a warning for the user, if any.
46
+ *
47
+ * SMI-6529 round 9: destination locks only serialize calls for one
48
+ * destination, so fan-outs of different skills loaded, changed and saved the
49
+ * manifest at the same time, through one shared temp file; twelve concurrent
50
+ * fan-outs kept one record, and could leave the file corrupt. Callers may
51
+ * hold a destination lock when they call this, but no destination lock is
52
+ * ever requested while this lock is held, so the two can't deadlock.
53
+ *
54
+ * A corrupt file used to be replaced with an empty manifest, dropping every
55
+ * other skill's record. It is now moved aside, with a warning; a file that
56
+ * can't be read at all is left alone and the call fails.
57
+ */
58
+ export declare function updateManifest(change: (manifest: LinkManifest) => void): Promise<string | undefined>;
59
+ /**
60
+ * Persist the manifest atomically (write-temp + rename) so a crash mid-write
61
+ * never leaves the file in a partial state.
62
+ */
63
+ export declare function saveManifest(manifest: LinkManifest): Promise<void>;
64
+ //# sourceMappingURL=fan-out.manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.manifest.d.ts","sourceRoot":"","sources":["../../../src/install/fan-out.manifest.ts"],"names":[],"mappings":"AAiBA,mFAAmF;AACnF,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;AAEzC,qFAAqF;AACrF,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,CAAC,CAAA;IACV,KAAK,EAAE,UAAU,EAAE,CAAA;CACpB;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,CAAA;IAClD,QAAQ,EAAE,YAAY,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,YAAY,CAAC,CA4B9D;AAED,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,CAKzE;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,CAE1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,GACvC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmB7B;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAYxE"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * SMI-4578 / SMI-6529: the fan-out link manifest
3
+ * (`~/.skillsmith/links/manifest.json`), split out of fan-out.ts to stay
4
+ * under the 500-line gate.
5
+ *
6
+ * Reads are best-effort. Every change goes through {@link updateManifest},
7
+ * which takes the manifest's own lock and never overwrites a file it couldn't
8
+ * read.
9
+ *
10
+ * @module @skillsmith/core/install/fan-out.manifest
11
+ */
12
+ import { homedir } from 'node:os';
13
+ import * as path from 'node:path';
14
+ import * as fsp from 'node:fs/promises';
15
+ import { assertNotRealUserHome } from '../services/skill-manifest.js';
16
+ import { withFileLock } from './fan-out.overwrite.js';
17
+ const MANIFEST_VERSION = 1;
18
+ /**
19
+ * Resolve the manifest file path. Sits under `~/.skillsmith/` so it shares
20
+ * the existing allow-list entry for that directory (DEFAULT_ALLOWED_DIRS in
21
+ * `pathValidation.ts`).
22
+ */
23
+ export function getLinkManifestPath() {
24
+ return path.join(homedir(), '.skillsmith', 'links', 'manifest.json');
25
+ }
26
+ /**
27
+ * Read the manifest and report its state. Valid JSON with a higher version is
28
+ * `unreadable`, not `corrupt`: a newer Skillsmith wrote it, and moving it
29
+ * aside would drop its records too (SMI-6529 round 10).
30
+ */
31
+ export async function readManifestFile() {
32
+ const empty = { version: MANIFEST_VERSION, links: [] };
33
+ let raw;
34
+ try {
35
+ raw = await fsp.readFile(getLinkManifestPath(), 'utf-8');
36
+ }
37
+ catch (err) {
38
+ const code = err.code;
39
+ if (code === 'ENOENT')
40
+ return { state: 'missing', manifest: empty };
41
+ return { state: 'unreadable', manifest: empty, reason: code ?? String(err) };
42
+ }
43
+ try {
44
+ // Round 11: a byte-order mark some editors add isn't corruption.
45
+ const text = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
46
+ const parsed = JSON.parse(text);
47
+ if (parsed?.version === MANIFEST_VERSION && Array.isArray(parsed.links)) {
48
+ return { state: 'ok', manifest: parsed };
49
+ }
50
+ if (typeof parsed?.version === 'number' && parsed.version > MANIFEST_VERSION) {
51
+ return {
52
+ state: 'unreadable',
53
+ manifest: empty,
54
+ reason: `version ${parsed.version}, written by a newer Skillsmith`,
55
+ };
56
+ }
57
+ }
58
+ catch {
59
+ // Not JSON: reported as corrupt below.
60
+ }
61
+ return { state: 'corrupt', manifest: empty };
62
+ }
63
+ /** Error for a manifest that exists but can't be read; nothing is written over it. */
64
+ export function unreadableManifestError(reason) {
65
+ return new Error(`could not read the fan-out link manifest at ${getLinkManifestPath()} (${reason}); ` +
66
+ `refusing to overwrite it.`);
67
+ }
68
+ /**
69
+ * Load the manifest for reading. Returns an empty manifest if the file is
70
+ * missing, unreadable or corrupt — fan-out is best-effort, never crash on
71
+ * cold start. Changes go through {@link updateManifest}, which never
72
+ * overwrites a file it couldn't read.
73
+ */
74
+ export async function loadManifest() {
75
+ return (await readManifestFile()).manifest;
76
+ }
77
+ /**
78
+ * Apply `change` to the manifest and save it, under the manifest's own lock.
79
+ * Returns a warning for the user, if any.
80
+ *
81
+ * SMI-6529 round 9: destination locks only serialize calls for one
82
+ * destination, so fan-outs of different skills loaded, changed and saved the
83
+ * manifest at the same time, through one shared temp file; twelve concurrent
84
+ * fan-outs kept one record, and could leave the file corrupt. Callers may
85
+ * hold a destination lock when they call this, but no destination lock is
86
+ * ever requested while this lock is held, so the two can't deadlock.
87
+ *
88
+ * A corrupt file used to be replaced with an empty manifest, dropping every
89
+ * other skill's record. It is now moved aside, with a warning; a file that
90
+ * can't be read at all is left alone and the call fails.
91
+ */
92
+ export async function updateManifest(change) {
93
+ const manifestPath = getLinkManifestPath();
94
+ assertNotRealUserHome(manifestPath, 'write');
95
+ await fsp.mkdir(path.dirname(manifestPath), { recursive: true });
96
+ return withFileLock(manifestPath, 'fan-out link manifest lock', async () => {
97
+ const read = await readManifestFile();
98
+ if (read.state === 'unreadable')
99
+ throw unreadableManifestError(read.reason);
100
+ let warning;
101
+ if (read.state === 'corrupt') {
102
+ const aside = `${manifestPath}.corrupt-${Date.now()}`;
103
+ await fsp.rename(manifestPath, aside);
104
+ warning =
105
+ `the fan-out link manifest at ${manifestPath} could not be parsed; it was moved to ` +
106
+ `${aside} and a new one started. Copies recorded only in the old file are no longer tracked.`;
107
+ }
108
+ change(read.manifest);
109
+ await saveManifest(read.manifest);
110
+ return warning;
111
+ });
112
+ }
113
+ /**
114
+ * Persist the manifest atomically (write-temp + rename) so a crash mid-write
115
+ * never leaves the file in a partial state.
116
+ */
117
+ export async function saveManifest(manifest) {
118
+ const manifestPath = getLinkManifestPath();
119
+ // SMI-6343 follow-up (adversarial review): a fourth parallel manifest-write
120
+ // implementation (a different file, `links/manifest.json`, but the same
121
+ // homedir-derived-with-no-override shape). The $HOME sandbox
122
+ // (vitest.setup.ts) was this file's only defense until this guard.
123
+ assertNotRealUserHome(manifestPath, 'write');
124
+ const dir = path.dirname(manifestPath);
125
+ await fsp.mkdir(dir, { recursive: true });
126
+ const tmp = `${manifestPath}.${process.pid}.tmp`;
127
+ await fsp.writeFile(tmp, JSON.stringify(manifest, null, 2), 'utf-8');
128
+ await fsp.rename(tmp, manifestPath);
129
+ }
130
+ //# sourceMappingURL=fan-out.manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.manifest.js","sourceRoot":"","sources":["../../../src/install/fan-out.manifest.ts"],"names":[],"mappings":"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"}