@skillsmith/core 0.12.3 → 0.12.5

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 (188) hide show
  1. package/CHANGELOG.md +168 -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 +1 -1
  27. package/dist/src/index.js +1 -1
  28. package/dist/src/install/fan-out.cleanup.d.ts +37 -0
  29. package/dist/src/install/fan-out.cleanup.d.ts.map +1 -0
  30. package/dist/src/install/fan-out.cleanup.js +75 -0
  31. package/dist/src/install/fan-out.cleanup.js.map +1 -0
  32. package/dist/src/install/fan-out.d.ts +41 -31
  33. package/dist/src/install/fan-out.d.ts.map +1 -1
  34. package/dist/src/install/fan-out.js +213 -112
  35. package/dist/src/install/fan-out.js.map +1 -1
  36. package/dist/src/install/fan-out.leftovers.d.ts +60 -0
  37. package/dist/src/install/fan-out.leftovers.d.ts.map +1 -0
  38. package/dist/src/install/fan-out.leftovers.js +251 -0
  39. package/dist/src/install/fan-out.leftovers.js.map +1 -0
  40. package/dist/src/install/fan-out.locks.d.ts +12 -0
  41. package/dist/src/install/fan-out.locks.d.ts.map +1 -0
  42. package/dist/src/install/fan-out.locks.js +114 -0
  43. package/dist/src/install/fan-out.locks.js.map +1 -0
  44. package/dist/src/install/fan-out.manifest.d.ts +64 -0
  45. package/dist/src/install/fan-out.manifest.d.ts.map +1 -0
  46. package/dist/src/install/fan-out.manifest.js +130 -0
  47. package/dist/src/install/fan-out.manifest.js.map +1 -0
  48. package/dist/src/install/fan-out.overwrite.d.ts +51 -0
  49. package/dist/src/install/fan-out.overwrite.d.ts.map +1 -0
  50. package/dist/src/install/fan-out.overwrite.js +343 -0
  51. package/dist/src/install/fan-out.overwrite.js.map +1 -0
  52. package/dist/src/install/remove-if-same.d.ts +58 -0
  53. package/dist/src/install/remove-if-same.d.ts.map +1 -0
  54. package/dist/src/install/remove-if-same.js +222 -0
  55. package/dist/src/install/remove-if-same.js.map +1 -0
  56. package/dist/src/provenance/backfill.d.ts.map +1 -1
  57. package/dist/src/provenance/backfill.js +15 -2
  58. package/dist/src/provenance/backfill.js.map +1 -1
  59. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
  60. package/dist/src/security/scanner/SecurityScanner.scanners.js +8 -1
  61. package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
  62. package/dist/src/security/scanner/SecurityScanner.value-gate.d.ts +22 -2
  63. package/dist/src/security/scanner/SecurityScanner.value-gate.d.ts.map +1 -1
  64. package/dist/src/security/scanner/SecurityScanner.value-gate.js +143 -6
  65. package/dist/src/security/scanner/SecurityScanner.value-gate.js.map +1 -1
  66. package/dist/src/security/scanner/SecurityScanner.weak-passwords.d.ts +2 -2
  67. package/dist/src/security/scanner/SecurityScanner.weak-passwords.d.ts.map +1 -1
  68. package/dist/src/security/scanner/SecurityScanner.weak-passwords.js +185 -190
  69. package/dist/src/security/scanner/SecurityScanner.weak-passwords.js.map +1 -1
  70. package/dist/src/security/scanner/patterns.d.ts +47 -2
  71. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  72. package/dist/src/security/scanner/patterns.js +48 -3
  73. package/dist/src/security/scanner/patterns.js.map +1 -1
  74. package/dist/src/security/scanner/patterns.sensitive-path.d.ts +31 -3
  75. package/dist/src/security/scanner/patterns.sensitive-path.d.ts.map +1 -1
  76. package/dist/src/security/scanner/patterns.sensitive-path.js +59 -4
  77. package/dist/src/security/scanner/patterns.sensitive-path.js.map +1 -1
  78. package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -1
  79. package/dist/src/services/agent-pack/prompt-source.js +3 -2
  80. package/dist/src/services/agent-pack/prompt-source.js.map +1 -1
  81. package/dist/src/services/skill-installation.content.d.ts +7 -2
  82. package/dist/src/services/skill-installation.content.d.ts.map +1 -1
  83. package/dist/src/services/skill-installation.content.js +75 -28
  84. package/dist/src/services/skill-installation.content.js.map +1 -1
  85. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  86. package/dist/src/services/skill-installation.helpers.js +8 -0
  87. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  88. package/dist/src/services/skill-installation.io.d.ts +1 -29
  89. package/dist/src/services/skill-installation.io.d.ts.map +1 -1
  90. package/dist/src/services/skill-installation.io.dirs.d.ts +15 -0
  91. package/dist/src/services/skill-installation.io.dirs.d.ts.map +1 -0
  92. package/dist/src/services/skill-installation.io.dirs.js +84 -0
  93. package/dist/src/services/skill-installation.io.dirs.js.map +1 -0
  94. package/dist/src/services/skill-installation.io.js +6 -176
  95. package/dist/src/services/skill-installation.io.js.map +1 -1
  96. package/dist/src/services/skill-installation.io.rollback.d.ts +138 -0
  97. package/dist/src/services/skill-installation.io.rollback.d.ts.map +1 -0
  98. package/dist/src/services/skill-installation.io.rollback.js +218 -0
  99. package/dist/src/services/skill-installation.io.rollback.js.map +1 -0
  100. package/dist/src/services/skill-installation.io.write.d.ts +69 -0
  101. package/dist/src/services/skill-installation.io.write.d.ts.map +1 -0
  102. package/dist/src/services/skill-installation.io.write.js +417 -0
  103. package/dist/src/services/skill-installation.io.write.js.map +1 -0
  104. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  105. package/dist/src/services/skill-installation.service.finalize.d.ts +62 -0
  106. package/dist/src/services/skill-installation.service.finalize.d.ts.map +1 -0
  107. package/dist/src/services/skill-installation.service.finalize.js +136 -0
  108. package/dist/src/services/skill-installation.service.finalize.js.map +1 -0
  109. package/dist/src/services/skill-installation.service.js +66 -80
  110. package/dist/src/services/skill-installation.service.js.map +1 -1
  111. package/dist/src/services/skill-installation.target-guard.d.ts +82 -0
  112. package/dist/src/services/skill-installation.target-guard.d.ts.map +1 -0
  113. package/dist/src/services/skill-installation.target-guard.js +410 -0
  114. package/dist/src/services/skill-installation.target-guard.js.map +1 -0
  115. package/dist/src/services/skill-installation.types.d.ts +13 -1
  116. package/dist/src/services/skill-installation.types.d.ts.map +1 -1
  117. package/dist/src/services/skill-installation.types.js.map +1 -1
  118. package/dist/src/services/skill-installation.uninstall.d.ts +3 -1
  119. package/dist/src/services/skill-installation.uninstall.d.ts.map +1 -1
  120. package/dist/src/services/skill-installation.uninstall.helpers.d.ts +59 -0
  121. package/dist/src/services/skill-installation.uninstall.helpers.d.ts.map +1 -0
  122. package/dist/src/services/skill-installation.uninstall.helpers.js +124 -0
  123. package/dist/src/services/skill-installation.uninstall.helpers.js.map +1 -0
  124. package/dist/src/services/skill-installation.uninstall.js +148 -25
  125. package/dist/src/services/skill-installation.uninstall.js.map +1 -1
  126. package/dist/src/utils/safe-fs.d.ts +69 -0
  127. package/dist/src/utils/safe-fs.d.ts.map +1 -1
  128. package/dist/src/utils/safe-fs.js +113 -2
  129. package/dist/src/utils/safe-fs.js.map +1 -1
  130. package/dist/tests/install/fan-out.test.js +1951 -13
  131. package/dist/tests/install/fan-out.test.js.map +1 -1
  132. package/dist/tests/install/remove-if-same.test.d.ts +2 -0
  133. package/dist/tests/install/remove-if-same.test.d.ts.map +1 -0
  134. package/dist/tests/install/remove-if-same.test.js +298 -0
  135. package/dist/tests/install/remove-if-same.test.js.map +1 -0
  136. package/dist/tests/integration/fan-out-lock-contention.test.d.ts +11 -0
  137. package/dist/tests/integration/fan-out-lock-contention.test.d.ts.map +1 -0
  138. package/dist/tests/integration/fan-out-lock-contention.test.js +53 -0
  139. package/dist/tests/integration/fan-out-lock-contention.test.js.map +1 -0
  140. package/dist/tests/provenance/backfill.test.js +43 -0
  141. package/dist/tests/provenance/backfill.test.js.map +1 -1
  142. package/dist/tests/security/scanner-regression-guard.test.js +33 -5
  143. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  144. package/dist/tests/security/sensitive-path-adversarial-review.test.js +120 -0
  145. package/dist/tests/security/sensitive-path-adversarial-review.test.js.map +1 -1
  146. package/dist/tests/security/sensitive-path-boolean-flag.test.d.ts +2 -0
  147. package/dist/tests/security/sensitive-path-boolean-flag.test.d.ts.map +1 -0
  148. package/dist/tests/security/sensitive-path-boolean-flag.test.js +151 -0
  149. package/dist/tests/security/sensitive-path-boolean-flag.test.js.map +1 -0
  150. package/dist/tests/security/sensitive-path-fp.test.js +71 -5
  151. package/dist/tests/security/sensitive-path-fp.test.js.map +1 -1
  152. package/dist/tests/security/sensitive-path-prefixed-secrets.test.d.ts +2 -0
  153. package/dist/tests/security/sensitive-path-prefixed-secrets.test.d.ts.map +1 -0
  154. package/dist/tests/security/sensitive-path-prefixed-secrets.test.js +176 -0
  155. package/dist/tests/security/sensitive-path-prefixed-secrets.test.js.map +1 -0
  156. package/dist/tests/security/weak-password-lexicon.test.js +14 -4
  157. package/dist/tests/security/weak-password-lexicon.test.js.map +1 -1
  158. package/dist/tests/unit/services/skill-installation.content.test.js +23 -0
  159. package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -1
  160. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +35 -14
  161. package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -1
  162. package/dist/tests/unit/services/skill-installation.io.rollback.test.d.ts +2 -0
  163. package/dist/tests/unit/services/skill-installation.io.rollback.test.d.ts.map +1 -0
  164. package/dist/tests/unit/services/skill-installation.io.rollback.test.js +984 -0
  165. package/dist/tests/unit/services/skill-installation.io.rollback.test.js.map +1 -0
  166. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.d.ts +2 -0
  167. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.d.ts.map +1 -0
  168. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.js +148 -0
  169. package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.js.map +1 -0
  170. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +19 -0
  171. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -1
  172. package/dist/tests/unit/services/skill-installation.service.finalize.test.d.ts +2 -0
  173. package/dist/tests/unit/services/skill-installation.service.finalize.test.d.ts.map +1 -0
  174. package/dist/tests/unit/services/skill-installation.service.finalize.test.js +147 -0
  175. package/dist/tests/unit/services/skill-installation.service.finalize.test.js.map +1 -0
  176. package/dist/tests/unit/services/skill-installation.service.test.js +43 -0
  177. package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
  178. package/dist/tests/unit/services/skill-installation.target-guard.test.d.ts +2 -0
  179. package/dist/tests/unit/services/skill-installation.target-guard.test.d.ts.map +1 -0
  180. package/dist/tests/unit/services/skill-installation.target-guard.test.js +590 -0
  181. package/dist/tests/unit/services/skill-installation.target-guard.test.js.map +1 -0
  182. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.d.ts +2 -0
  183. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.d.ts.map +1 -0
  184. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.js +473 -0
  185. package/dist/tests/unit/services/skill-installation.uninstall.guard.test.js.map +1 -0
  186. package/dist/tests/utils/safe-fs.test.js +129 -2
  187. package/dist/tests/utils/safe-fs.test.js.map +1 -1
  188. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ import type { Stats } from 'node:fs';
2
+ import type { LinkManifest } from './fan-out.manifest.js';
3
+ export { leftoverBackupWarning, listLeftoverBackups, recoverDestination, } from './fan-out.leftovers.js';
4
+ export { withDestinationLock, withFileLock } from './fan-out.locks.js';
5
+ /** Result of looking for `.git` at a directory's root. */
6
+ export type GitAtRoot = {
7
+ kind: 'absent';
8
+ } | {
9
+ kind: 'present';
10
+ } | {
11
+ kind: 'unknown';
12
+ reason: string;
13
+ };
14
+ /**
15
+ * Look for a `.git` entry at the root of `p` with `lstat` (never following a
16
+ * symlink, so a dangling `.git` link still counts). Any error other than
17
+ * ENOENT/ENOTDIR is `unknown`, which callers must treat as "refuse".
18
+ */
19
+ export declare function checkGitAtRoot(p: string): Promise<GitAtRoot>;
20
+ /** Refusal text for a git check that isn't `absent`, or `null` when it is. */
21
+ export declare function gitRefusal(p: string, check: GitAtRoot, verb: 'overwrite' | 'delete'): string | null;
22
+ /**
23
+ * Throw, changing nothing, unless the existing destination may be replaced:
24
+ * a symlink (always disposable), or a copy recorded in the link manifest that
25
+ * hasn't become a git working tree.
26
+ */
27
+ export declare function assertOverwritable(dest: string, existing: Stats, manifest: LinkManifest): Promise<void>;
28
+ /** What {@link replaceDestination} put in place, and what it could not clean up. */
29
+ export interface PlacedDestination {
30
+ /**
31
+ * Identity of the entry now at `dest`, or null when this call cannot be
32
+ * sure what it placed — round 21 (Opus): a published symlink that something
33
+ * replaced between creating it and reading it back. A null identity means an
34
+ * undo reports the write instead of deleting anything.
35
+ */
36
+ placed: Stats | null;
37
+ /** Warnings for the caller to surface, e.g. a superseded copy left behind. */
38
+ warnings: string[];
39
+ }
40
+ /**
41
+ * Replace (or create) `dest` with what `write` produces. The caller must hold
42
+ * `withDestinationLock(dest)` and have checked `assertOverwritable`. `write`
43
+ * receives a staging path; if it throws, the destination is never touched.
44
+ *
45
+ * Returns the identity of the entry now at `dest`. Round 15 (Opus): it is
46
+ * taken from the staged entry before the swap, since a rename keeps device
47
+ * and inode. Taken after the swap, it could record a folder another program
48
+ * had put there in between, and an undo would then delete that folder.
49
+ */
50
+ export declare function replaceDestination(dest: string, write: (stagedPath: string) => Promise<void>): Promise<PlacedDestination>;
51
+ //# sourceMappingURL=fan-out.overwrite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.overwrite.d.ts","sourceRoot":"","sources":["../../../src/install/fan-out.overwrite.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAazD,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,wBAAwB,CAAA;AAG/B,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAUtE,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CASlE;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CACxB,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,WAAW,GAAG,QAAQ,GAC3B,MAAM,GAAG,IAAI,CAUf;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,oFAAoF;AACpF,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,MAAM,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAC3C,OAAO,CAAC,iBAAiB,CAAC,CA0B5B"}
@@ -0,0 +1,343 @@
1
+ /**
2
+ * SMI-6529: safe replacement of a fan-out destination, split out of
3
+ * fan-out.ts to stay under the 500-line gate.
4
+ *
5
+ * Every write to a destination happens under a per-destination lock, and a
6
+ * refresh never writes into the destination in place:
7
+ *
8
+ * 1. The new copy (or symlink) is written into a hidden staging folder next
9
+ * to the destination.
10
+ * 2. The old destination is renamed aside into a hidden backup folder.
11
+ * 3. The staged copy is renamed into place, and the backup is dropped.
12
+ *
13
+ * If step 3 fails, the old destination is renamed back. Nothing at the
14
+ * destination is ever deleted to make room, and a half-written copy is never
15
+ * visible there.
16
+ *
17
+ * Review history:
18
+ * - Round 5 (R2/R3): the first version used a fixed backup name it
19
+ * `rm -rf`'d first, and never restored a backup stranded by a crash.
20
+ * - Round 6: two concurrent force-refreshes of one destination could lose
21
+ * the original. Call B's recovery "restored" call A's in-flight backup,
22
+ * then A's failure path deleted it. The per-destination lock serializes
23
+ * recovery, write and swap; staging removes A's delete entirely. Round 6
24
+ * also found that a stranded `original` was restored whatever it was (a
25
+ * symlink to /etc included), that an EACCES on the `.git` check was
26
+ * reported as "contains a .git directory", and that the name prefix could
27
+ * match a sibling skill's folders.
28
+ * - Round 13 (cross-model review): the destination lock keeps other
29
+ * Skillsmith calls out, not other programs. So a staging or backup folder
30
+ * is deleted only while it is still the folder we made (same device and
31
+ * inode). Round 14: a crashed write's staging folder is never deleted
32
+ * (neither its name nor its contents proves who owns it), only reported,
33
+ * and a folder left in place is named with the reason. Round 15: each
34
+ * delete parks the folder under a random name and checks it there
35
+ * (`removeIfSame`), closing the gap between the check and the delete,
36
+ * and `replaceDestination` returns the new entry's identity, taken before
37
+ * it became visible at the destination.
38
+ * - Round 8: a lock released mid-attempt was reported as corrupt, so
39
+ * contending callers failed at once; waiting on an orphaned reclaim lock
40
+ * still blocked the event loop; a reinstall after an uninstall restored a
41
+ * stale backup; and an empty backup folder from a crash was reported
42
+ * forever.
43
+ *
44
+ * @module @skillsmith/core/install/fan-out.overwrite
45
+ */
46
+ import * as path from 'node:path';
47
+ import * as fsp from 'node:fs/promises';
48
+ import { removeIfSame } from './remove-if-same.js';
49
+ import { BACKUP_TAG, STAGING_TAG, isRecordedCopy, lstatOrNull, siblingPrefix, } from './fan-out.leftovers.js';
50
+ // Round 16: the leftover-reporting half of this module moved to
51
+ // fan-out.leftovers.ts to stay under the 500-line gate. Re-exported here so
52
+ // existing importers keep one entry point.
53
+ export { leftoverBackupWarning, listLeftoverBackups, recoverDestination, } from './fan-out.leftovers.js';
54
+ // Round 27: the locking half moved to fan-out.locks.ts for the same 500-line
55
+ // reason. Re-exported here so existing importers keep one entry point.
56
+ export { withDestinationLock, withFileLock } from './fan-out.locks.js';
57
+ function errorMessage(err) {
58
+ return err instanceof Error ? err.message : String(err);
59
+ }
60
+ function errorCode(err) {
61
+ return err?.code ?? errorMessage(err);
62
+ }
63
+ /**
64
+ * Look for a `.git` entry at the root of `p` with `lstat` (never following a
65
+ * symlink, so a dangling `.git` link still counts). Any error other than
66
+ * ENOENT/ENOTDIR is `unknown`, which callers must treat as "refuse".
67
+ */
68
+ export async function checkGitAtRoot(p) {
69
+ try {
70
+ await fsp.lstat(path.join(p, '.git'));
71
+ return { kind: 'present' };
72
+ }
73
+ catch (err) {
74
+ const code = err.code;
75
+ if (code === 'ENOENT' || code === 'ENOTDIR')
76
+ return { kind: 'absent' };
77
+ return { kind: 'unknown', reason: code ?? errorMessage(err) };
78
+ }
79
+ }
80
+ /** Refusal text for a git check that isn't `absent`, or `null` when it is. */
81
+ export function gitRefusal(p, check, verb) {
82
+ if (check.kind === 'absent')
83
+ return null;
84
+ if (check.kind === 'present') {
85
+ return (`${p} is a recorded Skillsmith fan-out copy but now contains a .git directory ` +
86
+ `(looks like a real git working tree) — refusing to ${verb} it. Remove it yourself, ` +
87
+ `then retry.`);
88
+ }
89
+ return `could not check ${p} for a .git directory (${check.reason}) — refusing to ${verb} it.`;
90
+ }
91
+ /**
92
+ * Throw, changing nothing, unless the existing destination may be replaced:
93
+ * a symlink (always disposable), or a copy recorded in the link manifest that
94
+ * hasn't become a git working tree.
95
+ */
96
+ export async function assertOverwritable(dest, existing, manifest) {
97
+ if (existing.isSymbolicLink())
98
+ return;
99
+ if (!isRecordedCopy(dest, manifest)) {
100
+ // Round 23 (Opus): the non-force refusal sends the user here, so this one
101
+ // explains the same state — an empty directory is most likely a claim an
102
+ // interrupted install left, and Skillsmith will not remove it for them.
103
+ const empty = existing.isDirectory() && (await fsp.readdir(dest).catch(() => ['?'])).length === 0;
104
+ throw new Error(`addLink: ${dest} already exists and is not a fan-out destination Skillsmith recorded ` +
105
+ `(no matching entry in the link manifest); remove it yourself, then retry with force.` +
106
+ (empty
107
+ ? ` It is an empty directory: an interrupted install may have left it, and Skillsmith ` +
108
+ `does not remove it for you, since an empty directory you made looks the same.`
109
+ : ''));
110
+ }
111
+ const refusal = gitRefusal(dest, await checkGitAtRoot(dest), 'overwrite');
112
+ if (refusal)
113
+ throw new Error(`addLink: ${refusal}`);
114
+ }
115
+ /**
116
+ * Replace (or create) `dest` with what `write` produces. The caller must hold
117
+ * `withDestinationLock(dest)` and have checked `assertOverwritable`. `write`
118
+ * receives a staging path; if it throws, the destination is never touched.
119
+ *
120
+ * Returns the identity of the entry now at `dest`. Round 15 (Opus): it is
121
+ * taken from the staged entry before the swap, since a rename keeps device
122
+ * and inode. Taken after the swap, it could record a folder another program
123
+ * had put there in between, and an undo would then delete that folder.
124
+ */
125
+ export async function replaceDestination(dest, write) {
126
+ const parent = path.dirname(dest);
127
+ const stagingFolder = await fsp.mkdtemp(path.join(parent, siblingPrefix(dest, STAGING_TAG)));
128
+ const made = await fsp.lstat(stagingFolder);
129
+ const staged = path.join(stagingFolder, 'content');
130
+ let placed;
131
+ let warnings;
132
+ try {
133
+ await write(staged);
134
+ const staging = await fsp.lstat(staged);
135
+ ({ placed, warnings } = await swapIntoPlace(dest, staged, staging));
136
+ }
137
+ catch (err) {
138
+ // A partial copy made from the source skill: remove it, but only while
139
+ // the folder is still the one we made (round 13), and say why when that
140
+ // didn't happen (round 14).
141
+ const removal = await removeIfSame(stagingFolder, made);
142
+ if (removal.removed)
143
+ throw err;
144
+ throw new Error(`${errorMessage(err)}; the staging folder ${stagingFolder} ${removal.reason}`, {
145
+ cause: err,
146
+ });
147
+ }
148
+ // Empty after a successful swap, so a non-recursive rmdir can't delete
149
+ // anything else. If it fails, the folder stays, and listLeftoverBackups,
150
+ // which the caller runs next, reports it.
151
+ await fsp.rmdir(stagingFolder).catch(() => { });
152
+ return { placed, warnings };
153
+ }
154
+ /**
155
+ * Put `staged` at `dest` without replacing anything that holds content. Round
156
+ * 20 (cross-model review): `rename` replaces its destination, so publishing
157
+ * with it could destroy an entry another program created after the destination
158
+ * was checked. A directory is published by claiming the name with `mkdir`,
159
+ * which fails with EEXIST rather than replacing, and then renaming the staged
160
+ * copy over that empty claim; a symlink is published with `symlink()`, which
161
+ * refuses an occupied path the same way.
162
+ *
163
+ * Round 24: the bound this holds, and the one it does not. The rename can
164
+ * still replace an EMPTY directory, if another program removes this call's own
165
+ * claim and substitutes one inside the two-syscall window — the accepted
166
+ * residual described below (SMI-6559). It can never replace anything holding
167
+ * content: POSIX `rename` refuses a non-empty directory (ENOTEMPTY) and a
168
+ * non-directory (ENOTDIR).
169
+ *
170
+ * Returns the identity of what is now at `dest`: a rename keeps the staged
171
+ * directory's, while a published symlink is a new entry.
172
+ */
173
+ async function publish(staged, dest, staging) {
174
+ if (staging.isSymbolicLink()) {
175
+ const target = await fsp.readlink(staged);
176
+ await fsp.symlink(target, dest);
177
+ await fsp.unlink(staged).catch(() => { });
178
+ // Round 21 (Opus): `symlink` creates a NEW entry, so unlike the rename
179
+ // below there is no pre-image to report. Read it back and accept it only
180
+ // while it is still the link just written — anything else means another
181
+ // program replaced it in that window, and reporting that identity would
182
+ // let an undo delete their entry. Fail closed: no identity. (`link` is not
183
+ // an option: on Linux it links the symlink, on macOS it follows it.)
184
+ const now = await lstatOrNull(dest);
185
+ if (now === null || !now.isSymbolicLink())
186
+ return null;
187
+ const linked = await fsp.readlink(dest).catch(() => null);
188
+ return linked === target ? now : null;
189
+ }
190
+ // Round 22 (cross-model review; accepted residual, user decision
191
+ // 2026-09-12, tracked as SMI-6559): the claim and the rename below are two
192
+ // syscalls, and that CANNOT be made atomic here — Node exposes no
193
+ // no-replace rename (Linux's renameat2(RENAME_NOREPLACE) and macOS's
194
+ // renamex_np have no binding in node:fs). What the remaining window
195
+ // requires is narrow: another program must remove THIS call's own empty
196
+ // claim and put its own entry at the path in between — and even then the
197
+ // rename can only ever replace an EMPTY directory, because POSIX `rename`
198
+ // refuses a non-empty directory (ENOTEMPTY) and a non-directory (ENOTDIR).
199
+ // Measured across six interleavings on macOS and Linux, three of which
200
+ // removed the claim first: nothing holding content was ever replaced. The alternative —
201
+ // filling the claimed directory in place — removes the window but makes a
202
+ // half-copied skill visible at the destination, which staging exists to
203
+ // prevent (round 6).
204
+ await fsp.mkdir(dest);
205
+ try {
206
+ await fsp.rename(staged, dest);
207
+ }
208
+ catch (err) {
209
+ // Round 25 (cross-model review): the empty directory this call claimed is
210
+ // left in place and reported, never removed. Round 24 removed it after an
211
+ // identity check, and that check cannot hold: `mkdir` and the `lstat` that
212
+ // takes the identity are two syscalls, so what it records may already be
213
+ // another program's directory; the check and the `rmdir` are two more, so a
214
+ // substitution between them can reuse the freed inode and match. Node
215
+ // exposes no pathname operation that verifies identity and removes in one
216
+ // step, so the only choices are to remove something we cannot prove is
217
+ // ours, or to leave it and say so. This module's rule decides it: delete
218
+ // only what you can identify. The cost is an empty directory at the
219
+ // destination, which the next install refuses and explains.
220
+ throw new Error(`${errorMessage(err)}; the empty directory this call claimed at ${dest} was left in ` +
221
+ `place, since nothing can prove it is still the one this call made. Remove it ` +
222
+ `yourself if you do not want it.`, { cause: err });
223
+ }
224
+ return staging;
225
+ }
226
+ /** Swaps `staged` into `dest`, returning what it placed and what it could not clean up. */
227
+ async function swapIntoPlace(dest, staged, staging) {
228
+ const existing = await lstatOrNull(dest);
229
+ if (existing === null) {
230
+ return { placed: await publish(staged, dest, staging), warnings: [] };
231
+ }
232
+ if (existing.isSymbolicLink()) {
233
+ // Round 19 (Opus): this used to `unlink` the destination two syscalls
234
+ // after the `lstat` that said "symlink", so a file another program put at
235
+ // the path in between was deleted. `removeIfSame` removes only the entry
236
+ // it checked.
237
+ const removal = await removeIfSame(dest, existing);
238
+ if (!removal.removed)
239
+ throw new Error(`addLink: ${dest} ${removal.reason}.`);
240
+ return { placed: await publish(staged, dest, staging), warnings: [] };
241
+ }
242
+ const backupFolder = await fsp.mkdtemp(path.join(path.dirname(dest), siblingPrefix(dest, BACKUP_TAG)));
243
+ const backupMade = await fsp.lstat(backupFolder);
244
+ const original = path.join(backupFolder, 'original');
245
+ // Round 20 (cross-model review): move aside only the entry that was checked.
246
+ // A rename moves whatever is at the path, so without this an entry another
247
+ // program had just put there would be carried into this call's backup folder.
248
+ // The same accepted residual as the claim above (SMI-6559): this check and
249
+ // the rename that follows are two syscalls. If another program replaces the
250
+ // destination in between, its entry is moved into this call's backup folder
251
+ // rather than deleted, and `listLeftoverBackups` reports it.
252
+ // Round 27 (cross-model review): a failed look used to fall into the
253
+ // refusal below, which told the user the destination "was replaced by
254
+ // something else" — a diagnosis this call never made. Refuse the same way,
255
+ // fail closed the same way, but say what actually happened.
256
+ let stillThere;
257
+ try {
258
+ stillThere = await lstatOrNull(dest);
259
+ }
260
+ catch (err) {
261
+ await fsp.rmdir(backupFolder).catch(() => { });
262
+ throw new Error(`addLink: ${dest} could not be checked (${errorCode(err)}) before this refresh could move ` +
263
+ `it aside; nothing was changed.`, { cause: err });
264
+ }
265
+ if (stillThere === null || stillThere.dev !== existing.dev || stillThere.ino !== existing.ino) {
266
+ await fsp.rmdir(backupFolder).catch(() => { });
267
+ throw new Error(`addLink: ${dest} was replaced by something else before this refresh could move it ` +
268
+ `aside; nothing was changed.`);
269
+ }
270
+ try {
271
+ await fsp.rename(dest, original);
272
+ }
273
+ catch (err) {
274
+ await fsp.rmdir(backupFolder).catch(() => { });
275
+ throw err;
276
+ }
277
+ let placed;
278
+ try {
279
+ placed = await publish(staged, dest, staging);
280
+ }
281
+ catch (err) {
282
+ // Round 19 (Opus): put the original back only while the path is still
283
+ // free — a rename would otherwise replace whatever took it, and the
284
+ // original is safe where it is, in a backup folder listLeftoverBackups
285
+ // reports. A path that can't be checked counts as taken.
286
+ let free;
287
+ try {
288
+ free = (await lstatOrNull(dest)) === null;
289
+ }
290
+ catch {
291
+ free = false;
292
+ }
293
+ if (!free) {
294
+ throw new Error(`addLink: ${errorMessage(err)}; something else is at ${dest} now, so the original was ` +
295
+ `left at ${original}`, { cause: err });
296
+ }
297
+ try {
298
+ await fsp.rename(original, dest);
299
+ }
300
+ catch (restoreErr) {
301
+ throw new Error(`addLink: ${errorMessage(err)}; the original could not be put back and is preserved ` +
302
+ `at ${original} (${errorMessage(restoreErr)})`, { cause: err });
303
+ }
304
+ await fsp.rmdir(backupFolder).catch(() => { });
305
+ throw err;
306
+ }
307
+ // The swap succeeded, so the superseded copy is no longer needed. Drop it
308
+ // only while the backup folder and the copy in it are still what we put
309
+ // there (round 13). A folder left for any reason (not ours, or the removal
310
+ // failed) stays, and listLeftoverBackups, which the caller runs next,
311
+ // reports it.
312
+ // Round 16 (cross-model review): say why a superseded copy was left, rather
313
+ // than dropping the reason and leaving a later sweep to call it "an earlier
314
+ // copy" with no explanation.
315
+ let originalNow;
316
+ try {
317
+ originalNow = await lstatOrNull(original);
318
+ }
319
+ catch (err) {
320
+ return {
321
+ placed,
322
+ warnings: [
323
+ `the copy this refresh replaced could not be checked (${errorCode(err)}), so ` +
324
+ `${backupFolder} was left in place; check it, then delete it yourself if you don't ` +
325
+ `need it.`,
326
+ ],
327
+ };
328
+ }
329
+ if (originalNow === null ||
330
+ originalNow.dev !== existing.dev ||
331
+ originalNow.ino !== existing.ino) {
332
+ // Not what we put there. listLeftoverBackups reports it.
333
+ return { placed, warnings: [] };
334
+ }
335
+ const removal = await removeIfSame(backupFolder, backupMade);
336
+ return {
337
+ placed,
338
+ warnings: removal.removed
339
+ ? []
340
+ : [`the copy this refresh replaced was kept: ${backupFolder} ${removal.reason}.`],
341
+ };
342
+ }
343
+ //# sourceMappingURL=fan-out.overwrite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.overwrite.js","sourceRoot":"","sources":["../../../src/install/fan-out.overwrite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /** Tag in a parked entry's name: `.<name>.skillsmith-removing-<32 hex>`. */
2
+ export declare const PARK_TAG = ".skillsmith-removing-";
3
+ /** An entry's device and inode, to tell it apart from anything later put at its path. */
4
+ export interface EntryIdentity {
5
+ dev: number;
6
+ ino: number;
7
+ }
8
+ /**
9
+ * Result of {@link removeIfSame}. `reason` reads after the path, e.g.
10
+ * `${target} ${reason}`.
11
+ */
12
+ export type CheckedRemoval = {
13
+ removed: true;
14
+ } | {
15
+ removed: false;
16
+ reason: string;
17
+ };
18
+ /** What a parked-leftover scan found, and whether it could look at all. */
19
+ export interface ParkedScan {
20
+ /** Entries a crashed or failed removal left parked beside the target. */
21
+ parked: string[];
22
+ /** Set when the folder holding them could not be listed. */
23
+ unreadable?: string;
24
+ }
25
+ /** Matches exactly the names {@link removeIfSame} parks `target` under. */
26
+ export declare function parkedPattern(target: string): RegExp;
27
+ /**
28
+ * Entries a crashed or failed removal left parked next to `target`, so a
29
+ * caller can report them. Round 16 (both reviewers): away from a fan-out
30
+ * destination nothing swept these, so a failed removal's leftover was named
31
+ * once, in one error, and never again.
32
+ */
33
+ export declare function listParkedLeftovers(target: string): Promise<ParkedScan>;
34
+ /**
35
+ * User-facing warning for what {@link listParkedLeftovers} found. Round 18
36
+ * (cross-model review): who owns a parked entry is not known. It is whatever
37
+ * was at the path when a removal moved it aside, which in a race can belong
38
+ * to another program, so the wording says that instead of calling it ours.
39
+ */
40
+ export declare function parkedLeftoverWarning(parked: string): string;
41
+ /**
42
+ * Remove `target` (a folder recursively, anything else with `unlink`) only if
43
+ * it is still the entry `expected` describes. An entry that is already gone
44
+ * counts as removed. Anything else is left alone, and whatever this call
45
+ * moved aside is reported with its exact path.
46
+ *
47
+ * Round 17 (cross-model review): nothing is ever renamed back. `rename`
48
+ * replaces what is at the destination — measured on macOS and Linux, a
49
+ * directory replaces an empty directory and a file or symlink replaces a file
50
+ * or symlink — so putting an entry back could destroy something created at
51
+ * that path in the meantime, and a check first only narrows that window
52
+ * rather than closing it. The two failure modes are not equal: a put-back can
53
+ * destroy an entry, while leaving one parked merely moves it, recoverably,
54
+ * and says where it went. The identity is also checked once BEFORE the entry
55
+ * is parked, so the ordinary mismatch moves nothing at all.
56
+ */
57
+ export declare function removeIfSame(target: string, expected: EntryIdentity): Promise<CheckedRemoval>;
58
+ //# sourceMappingURL=remove-if-same.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove-if-same.d.ts","sourceRoot":"","sources":["../../../src/install/remove-if-same.ts"],"names":[],"mappings":"AA0BA,4EAA4E;AAC5E,eAAO,MAAM,QAAQ,0BAA0B,CAAA;AAE/C,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAOnF,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAcD,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAoB7E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,cAAc,CAAC,CAuFzB"}