@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,124 @@
1
+ /**
2
+ * @fileoverview Helpers for `performUninstall`: what it found before removing,
3
+ * progress reporting that cannot change the outcome, record comparison, and
4
+ * the parked-leftover warnings.
5
+ * @module @skillsmith/core/services/skill-installation.uninstall.helpers
6
+ *
7
+ * Split out of `skill-installation.uninstall.ts` in SMI-6529 round 25, when
8
+ * that file reached 504 lines against the 500-line standard — the same
9
+ * sibling-split convention as `skill-installation.io.ts`. These are the pieces
10
+ * `performUninstall` calls but does not itself define; nothing here is part of
11
+ * `@skillsmith/core`'s public export surface.
12
+ */
13
+ import * as fs from 'fs/promises';
14
+ import { checkGitAtRoot } from '../install/fan-out.overwrite.js';
15
+ import { listParkedLeftovers, parkedLeftoverWarning } from '../install/remove-if-same.js';
16
+ /**
17
+ * SMI-6529 round 15 (cross-model review, Critical): what uninstall found at
18
+ * `installPath` before removing it. A folder with `.git` at its root is a git
19
+ * working tree, which git owns (ADR-155), so it is refused with or without
20
+ * `force`: deleting it would take unpushed commits and uncommitted edits with
21
+ * it. A symlink or a file needs no such check, since removing it leaves what
22
+ * it points at alone. Returns the entry's identity (null when nothing is
23
+ * there), so the delete can confirm it is still the same entry.
24
+ */
25
+ export async function inspectForRemoval(installPath) {
26
+ let stat;
27
+ try {
28
+ stat = await fs.lstat(installPath);
29
+ }
30
+ catch (error) {
31
+ const code = error.code;
32
+ if (code === 'ENOENT')
33
+ return { stat: null };
34
+ return {
35
+ refusal: 'Could not check ' +
36
+ installPath +
37
+ ' (' +
38
+ (code ?? String(error)) +
39
+ '); nothing was removed.',
40
+ };
41
+ }
42
+ if (!stat.isSymbolicLink() && !stat.isFile()) {
43
+ const git = await checkGitAtRoot(installPath);
44
+ if (git.kind === 'present') {
45
+ return {
46
+ refusal: installPath +
47
+ ' is a git working tree (it has .git at its root), so Skillsmith will not delete it, ' +
48
+ 'even with force. Check `git -C ' +
49
+ installPath +
50
+ ' status`, then remove the folder yourself.',
51
+ };
52
+ }
53
+ if (git.kind === 'unknown') {
54
+ return {
55
+ refusal: 'Could not check ' +
56
+ installPath +
57
+ ' for a .git directory (' +
58
+ git.reason +
59
+ '); nothing was removed.',
60
+ };
61
+ }
62
+ }
63
+ return { stat };
64
+ }
65
+ /**
66
+ * Report progress without letting a caller's listener change the outcome.
67
+ * Round 18 (cross-model review): a listener that threw after the folder was
68
+ * already removed reached the generic catch, which reported neither what had
69
+ * been removed nor what was left parked.
70
+ *
71
+ * Round 28 (pre-merge gate, PR-07): the failure is still swallowed as far as
72
+ * the uninstall's outcome goes — that part was deliberate and stays — but it
73
+ * is no longer swallowed as far as the USER goes. It is pushed onto
74
+ * `problems`, which the caller surfaces alongside its other warnings. An empty
75
+ * catch here was the same "could not tell" silence this wave removed
76
+ * everywhere else, in the one place that reports to the user for a living.
77
+ */
78
+ export function notify(onProgress, problems, ...args) {
79
+ try {
80
+ onProgress(...args);
81
+ }
82
+ catch (err) {
83
+ const stage = String(args[0] ?? 'unknown');
84
+ problems.push(`the progress listener threw while reporting the "${stage}" step ` +
85
+ `(${err instanceof Error ? err.message : String(err)}); the uninstall itself was not ` +
86
+ `affected.`);
87
+ }
88
+ }
89
+ /**
90
+ * Whether `entry` is still, field for field, the record this uninstall
91
+ * loaded. Round 18 (cross-model review): timestamps alone are not a unique
92
+ * generation token — two installs can share a millisecond, an older writer
93
+ * can omit them, and a copied record keeps them — so every field has to
94
+ * match. An install that claims the same name writes its own values, so any
95
+ * difference means a different generation. A durable generation id belongs
96
+ * with A1's manifest work (SMI-6531).
97
+ */
98
+ export function sameRecord(entry, loaded) {
99
+ return stableJson(entry) === stableJson(loaded);
100
+ }
101
+ /**
102
+ * JSON with every object's keys in a fixed order, so two records compare by
103
+ * value. Round 19 (Opus): comparing field by field with `!==` was correct only
104
+ * while every field is a string — one array or object field, written by a
105
+ * newer version or another tool, would never compare equal, and the record
106
+ * would be kept forever with a warning that says an install claimed the name.
107
+ */
108
+ function stableJson(value) {
109
+ return JSON.stringify(value, (_key, nested) => nested !== null && typeof nested === 'object' && !Array.isArray(nested)
110
+ ? Object.fromEntries(Object.entries(nested).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
111
+ : nested);
112
+ }
113
+ /**
114
+ * Warnings naming anything an earlier removal left parked next to
115
+ * `installPath`. Round 17 (cross-model review): every exit after the removal
116
+ * reports these, not only the successful one.
117
+ */
118
+ export async function parkedWarnings(installPath) {
119
+ const scan = await listParkedLeftovers(installPath);
120
+ const warnings = scan.parked.map(parkedLeftoverWarning);
121
+ // Round 25 (cross-model review): say when the scan could not look at all.
122
+ return scan.unreadable ? [...warnings, scan.unreadable] : warnings;
123
+ }
124
+ //# sourceMappingURL=skill-installation.uninstall.helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-installation.uninstall.helpers.js","sourceRoot":"","sources":["../../../src/services/skill-installation.uninstall.helpers.ts"],"names":[],"mappings":"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"}
@@ -5,15 +5,19 @@
5
5
  *
6
6
  * Split out of `skill-installation.helpers.ts` to stay under the 500-line
7
7
  * standard once ADR-139's adoption logic was added — mirrors the existing
8
- * `skill-installation.io.ts` sibling-split convention. `performUninstall`
8
+ * `skill-installation.io.ts` sibling-split convention. Round 25 split its own
9
+ * helpers out again, into `skill-installation.uninstall.helpers.ts`, for the
10
+ * same reason. `performUninstall`
9
11
  * has exactly one internal consumer (`skill-installation.service.ts`) and
10
12
  * is not part of `@skillsmith/core`'s public export surface.
11
13
  */
12
14
  import * as fs from 'fs/promises';
13
15
  import * as path from 'path';
16
+ import { removeIfSame } from '../install/remove-if-same.js';
14
17
  import { checkForModifications } from './skill-installation.io.js';
15
18
  import { hashContent, manifestKeyFor } from './skill-installation.helpers.js';
16
19
  import { CANONICAL_CLIENT } from '../install/paths.js';
20
+ import { inspectForRemoval, notify, parkedWarnings, sameRecord, } from './skill-installation.uninstall.helpers.js';
17
21
  /**
18
22
  * ADR-139 (SMI-6274 Wave 4): build a manifest entry for a skill found on
19
23
  * disk with no manifest record — "adoption." Every field is reconstructed
@@ -143,8 +147,19 @@ export async function adoptUntrackedSkillEntry(skillName, skillDirName, installP
143
147
  export async function performUninstall(params) {
144
148
  const { skillName, force, skillsDir, manifest, skillDependencyRepo, onProgress, client = CANONICAL_CLIENT, } = params;
145
149
  const manifestKey = manifestKeyFor(skillName, client);
150
+ // Round 28 (pre-merge gate, PR-07): a progress listener that throws must not
151
+ // change the outcome, but it must not vanish either. Every notify below
152
+ // records into this one array, and every exit that reports warnings carries
153
+ // it.
154
+ const listenerProblems = [];
155
+ // Round 29 (pre-merge gate, confirmation pass): `notify` fires before every
156
+ // exit below, so EVERY exit has to carry what the listener did — not just
157
+ // the ones that already had a warning to give. The previous round covered
158
+ // the reporting exits only, which left a listener failure vanishing on the
159
+ // refusal, not-installed, adoption-failure and outer-error paths.
160
+ const listenerWarning = () => listenerProblems.length > 0 ? { warning: listenerProblems.join(' ') } : {};
146
161
  try {
147
- onProgress('manifest', 'Loading manifest');
162
+ notify(onProgress, listenerProblems, 'manifest', 'Loading manifest');
148
163
  const manifestData = await manifest.load();
149
164
  let skillEntry = manifestData.installedSkills[manifestKey];
150
165
  let adopted = false;
@@ -153,8 +168,37 @@ export async function performUninstall(params) {
153
168
  try {
154
169
  await fs.access(potentialPath);
155
170
  }
156
- catch {
157
- return { success: false, skillName, message: 'Skill "' + skillName + '" is not installed.' };
171
+ catch (err) {
172
+ // Round 25 (cross-model review): only absence means "not installed".
173
+ // EACCES or EIO means we could not tell, and saying "not installed"
174
+ // sends the user away from a skill that is still on disk. Round 26
175
+ // (cross-model review): an error carrying no `code` at all is also
176
+ // "could not tell", and the previous guard let it fall through to "not
177
+ // installed" — the same false absence, one step further out. Only
178
+ // ENOENT is absence now, and the thrown value is read null-safely.
179
+ const code = err?.code;
180
+ if (code !== 'ENOENT') {
181
+ const detail = code ?? (err instanceof Error ? err.message : String(err));
182
+ return {
183
+ success: false,
184
+ skillName,
185
+ message: `Could not tell whether "${skillName}" is installed: ${potentialPath} could not be ` +
186
+ `checked (${detail}). Nothing was removed.`,
187
+ ...listenerWarning(),
188
+ };
189
+ }
190
+ return {
191
+ success: false,
192
+ skillName,
193
+ message: 'Skill "' + skillName + '" is not installed.',
194
+ ...listenerWarning(),
195
+ };
196
+ }
197
+ // SMI-6529 round 15: refuse a git working tree before adopting it, so a
198
+ // refusal writes nothing to the manifest.
199
+ const early = await inspectForRemoval(potentialPath);
200
+ if ('refusal' in early) {
201
+ return { success: false, skillName, message: early.refusal, ...listenerWarning() };
158
202
  }
159
203
  // ADR-139 (SMI-6274 Wave 4): a skill present on disk with no manifest
160
204
  // entry is ADOPTED — reconciled by writing a manifest entry derived
@@ -170,20 +214,29 @@ export async function performUninstall(params) {
170
214
  // real `install()` landing in that window could be silently
171
215
  // clobbered. `adopted` now reflects whether OUR guess actually won
172
216
  // (false when a concurrent writer's real entry was found instead).
173
- onProgress('adopt', 'Adopting untracked skill (no manifest entry found)');
217
+ notify(onProgress, listenerProblems, 'adopt', 'Adopting untracked skill (no manifest entry found)');
174
218
  const adoptResult = await adoptUntrackedSkillEntry(skillName, skillName, potentialPath, manifestKey, manifest);
175
219
  if ('adoptionError' in adoptResult) {
176
220
  // Only if adoption itself fails does the command error — naming the
177
221
  // skill, the path, and the manifest it tried to write (ADR-139
178
222
  // point 1's stated failure contract).
179
- return { success: false, skillName, message: adoptResult.adoptionError };
223
+ return {
224
+ success: false,
225
+ skillName,
226
+ message: adoptResult.adoptionError,
227
+ ...listenerWarning(),
228
+ };
180
229
  }
181
230
  skillEntry = adoptResult.entry;
182
231
  adopted = adoptResult.adopted;
183
232
  }
184
233
  const installPath = skillEntry.installPath;
234
+ const seen = await inspectForRemoval(installPath);
235
+ if ('refusal' in seen) {
236
+ return { success: false, skillName, message: seen.refusal, ...listenerWarning() };
237
+ }
185
238
  if (!force) {
186
- onProgress('check', 'Checking for modifications');
239
+ notify(onProgress, listenerProblems, 'check', 'Checking for modifications');
187
240
  const modified = await checkForModifications(installPath, skillEntry.installedAt);
188
241
  if (modified) {
189
242
  return {
@@ -192,17 +245,33 @@ export async function performUninstall(params) {
192
245
  message: 'Skill "' +
193
246
  skillName +
194
247
  '" has been modified since installation. Use force=true to remove anyway.',
195
- warning: 'Local modifications will be lost if you force uninstall.',
248
+ warning: ['Local modifications will be lost if you force uninstall.', ...listenerProblems]
249
+ .join(' ')
250
+ .trim(),
196
251
  };
197
252
  }
198
253
  }
199
- onProgress('remove', 'Removing skill directory');
200
- try {
201
- await fs.rm(installPath, { recursive: true, force: true });
202
- }
203
- catch (error) {
204
- if (error.code !== 'ENOENT')
205
- throw error;
254
+ notify(onProgress, listenerProblems, 'remove', 'Removing skill directory');
255
+ // SMI-6529 round 15 (cross-model review, Critical): remove only the entry
256
+ // checked above. Anything another program put there since is left in
257
+ // place, and so is the manifest entry, so the user can retry.
258
+ if (seen.stat !== null) {
259
+ const removal = await removeIfSame(installPath, seen.stat);
260
+ if (!removal.removed) {
261
+ const parked = [...(await parkedWarnings(installPath)), ...listenerProblems];
262
+ return {
263
+ success: false,
264
+ skillName,
265
+ message: 'Skill "' +
266
+ skillName +
267
+ '" was not removed: ' +
268
+ installPath +
269
+ ' ' +
270
+ removal.reason +
271
+ '.',
272
+ ...(parked.length > 0 && { warning: parked.join(' ') }),
273
+ };
274
+ }
206
275
  }
207
276
  try {
208
277
  skillDependencyRepo.clearAll(skillEntry.id);
@@ -210,7 +279,7 @@ export async function performUninstall(params) {
210
279
  catch {
211
280
  // Table may not exist pre-migration
212
281
  }
213
- onProgress('manifest', 'Updating manifest');
282
+ notify(onProgress, listenerProblems, 'manifest', 'Updating manifest');
214
283
  // SMI-6007: route the final mutation through updateSafely() (lock +
215
284
  // fresh re-read + save) instead of saving the `manifestData` snapshot
216
285
  // loaded above. That snapshot can be stale by the time we get here —
@@ -225,20 +294,73 @@ export async function performUninstall(params) {
225
294
  // skill while its uninstall is mid-flight) can still leave disk and
226
295
  // manifest inconsistent — only the unrelated-entry data loss is fixed
227
296
  // here, not full transactional safety across the whole method.
228
- await manifest.updateSafely((current) => {
229
- const next = { ...current, installedSkills: { ...current.installedSkills } };
230
- delete next.installedSkills[manifestKey];
231
- return next;
232
- });
233
- onProgress('done', 'Uninstall complete');
297
+ //
298
+ // Round 16 (cross-model review): the record is dropped only while it still
299
+ // describes the skill just removed. A concurrent install of the same name
300
+ // rewrites this key, and an unconditional delete would lose that install's
301
+ // record. And a manifest write that fails after the folder is gone is
302
+ // reported for what it is, rather than surfacing as a bare lock error.
303
+ let claimedByAnotherInstall = false;
304
+ try {
305
+ await manifest.updateSafely((current) => {
306
+ const entry = current.installedSkills[manifestKey];
307
+ // Round 17 (cross-model review): a reinstall can land at the SAME
308
+ // path, so the path alone does not identify the generation just
309
+ // removed — and comparing paths alone also keeps a stale record
310
+ // forever whenever the spelling differs (a case-insensitive volume, a
311
+ // symlinked parent). Round 18: the whole record identifies it.
312
+ const describesWhatWasRemoved = entry === undefined || sameRecord(entry, skillEntry);
313
+ if (!describesWhatWasRemoved) {
314
+ claimedByAnotherInstall = true;
315
+ return current;
316
+ }
317
+ const next = { ...current, installedSkills: { ...current.installedSkills } };
318
+ delete next.installedSkills[manifestKey];
319
+ return next;
320
+ });
321
+ }
322
+ catch (error) {
323
+ const parked = [...(await parkedWarnings(installPath)), ...listenerProblems];
324
+ return {
325
+ success: false,
326
+ skillName,
327
+ removedPath: installPath,
328
+ message: 'Skill "' +
329
+ skillName +
330
+ '" was removed from ' +
331
+ installPath +
332
+ ', but its record in ' +
333
+ manifest.path +
334
+ ' could not be updated (' +
335
+ (error instanceof Error ? error.message : String(error)) +
336
+ '). Its folder is already gone; run the same remove again once that file is writable, ' +
337
+ 'which acts on whatever that record names at the time.',
338
+ ...(parked.length > 0 && { warning: parked.join(' ') }),
339
+ };
340
+ }
341
+ notify(onProgress, listenerProblems, 'done', 'Uninstall complete');
342
+ // Round 16 (both reviewers): away from a fan-out destination nothing swept
343
+ // what a failed removal parked, so it was named once and never again.
344
+ const warnings = [
345
+ ...(adopted
346
+ ? [
347
+ 'This skill had no manifest entry (untracked) — it was adopted from disk state before removal.',
348
+ ]
349
+ : []),
350
+ ...(claimedByAnotherInstall
351
+ ? [
352
+ 'Another install claimed this name while this one was being removed, so that record was left alone.',
353
+ ]
354
+ : []),
355
+ ...(await parkedWarnings(installPath)),
356
+ ...listenerProblems,
357
+ ];
234
358
  return {
235
359
  success: true,
236
360
  skillName,
237
361
  message: 'Skill "' + skillName + '" has been uninstalled successfully.',
238
362
  removedPath: installPath,
239
- ...(adopted && {
240
- warning: 'This skill had no manifest entry (untracked) — it was adopted from disk state before removal.',
241
- }),
363
+ ...(warnings.length > 0 && { warning: warnings.join(' ') }),
242
364
  };
243
365
  }
244
366
  catch (error) {
@@ -246,6 +368,7 @@ export async function performUninstall(params) {
246
368
  success: false,
247
369
  skillName,
248
370
  message: error instanceof Error ? error.message : 'Unknown error during uninstall',
371
+ ...listenerWarning(),
249
372
  };
250
373
  }
251
374
  }
@@ -1 +1 @@
1
- {"version":3,"file":"skill-installation.uninstall.js","sourceRoot":"","sources":["../../../src/services/skill-installation.uninstall.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"skill-installation.uninstall.js","sourceRoot":"","sources":["../../../src/services/skill-installation.uninstall.ts"],"names":[],"mappings":"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"}
@@ -15,7 +15,35 @@
15
15
  * - SMI-2290: Checks nlink count to detect hardlinks
16
16
  * - SMI-2288: Properly handles string encoding options for permissions
17
17
  */
18
+ import type { FileHandle } from 'fs/promises';
18
19
  import type { WriteFileOptions } from 'fs';
20
+ /**
21
+ * O_NOFOLLOW: refuse to open symlinks
22
+ * Prefer runtime constant from fs.constants, fall back to platform-specific values
23
+ * if unavailable (e.g., Windows where O_NOFOLLOW is not defined).
24
+ *
25
+ * Exported (SMI-6529 F2) so other symlink-safe fd-based writers — e.g.
26
+ * writeInstallFiles' own rollback restore — share this exact platform
27
+ * -fallback resolution instead of a second, driftable copy.
28
+ */
29
+ export declare const O_NOFOLLOW: number;
30
+ /**
31
+ * SMI-6529 N3 (round 4): `FileHandle.write()` is permitted by POSIX (and
32
+ * measured live — 2048/10000 bytes under `RLIMIT_FSIZE`) to write FEWER
33
+ * bytes than requested WITHOUT throwing. Every fd-based writer in this
34
+ * module family used a single unchecked `await fd.write(data)` call, so a
35
+ * short write silently truncated content with no error — for
36
+ * `restoreSnapshots()` specifically, that meant a "successful" restore that
37
+ * left the file smaller than the true original with no `InstallRestoreError`
38
+ * ever raised. Loops until every byte is accounted for, or throws if a call
39
+ * makes zero progress (a short write that returns 0 can otherwise spin
40
+ * forever).
41
+ *
42
+ * @param position - Explicit starting file offset, or `null` to write
43
+ * sequentially from the handle's own current position (which auto-advances
44
+ * after each underlying `write()` call, short or not).
45
+ */
46
+ export declare function writeFullBuffer(fd: FileHandle, data: Buffer, position?: number | null): Promise<void>;
19
47
  /**
20
48
  * Error thrown when a symlink is detected at a write destination.
21
49
  */
@@ -60,4 +88,45 @@ export declare class HardlinkError extends Error {
60
88
  * ```
61
89
  */
62
90
  export declare function safeWriteFile(filePath: string, content: string | Buffer, options?: WriteFileOptions | string): Promise<void>;
91
+ /**
92
+ * Error thrown when `safeCreateFile`'s exclusive create finds the destination
93
+ * already occupied — created by something else between the caller's own
94
+ * pre-write classification and this call (a race, not a symlink/hardlink).
95
+ * SMI-6529 L14(b).
96
+ */
97
+ export declare class ExclusiveCreateRaceError extends Error {
98
+ readonly filePath: string;
99
+ constructor(filePath: string);
100
+ }
101
+ /**
102
+ * SMI-6529 L14(b): create a NEW file exclusively — refuses (throws
103
+ * {@link ExclusiveCreateRaceError}) if anything already exists at `filePath`,
104
+ * symlink or otherwise, instead of silently truncating/overwriting it. Only
105
+ * appropriate for a write target the caller has just classified as `absent`
106
+ * (`classifyPreWrite` in skill-installation.io.rollback.ts) — the atomicity
107
+ * of `O_CREAT|O_EXCL` means "did this call create the file" is knowable
108
+ * without needing `safeWriteFile`'s "record BEFORE writing" hedge (F1, round
109
+ * 1), which exists specifically to survive `O_TRUNC`'s non-atomicity on an
110
+ * OVERWRITE — a case this function is never used for.
111
+ *
112
+ * @param filePath - Absolute path to create (must not already exist)
113
+ * @param content - File content (string or Buffer)
114
+ * @param options - Optional fs.writeFile options (encoding, mode) or encoding string
115
+ * @param onCreated - SMI-6529 N9 (round 4): fired the MOMENT the exclusive
116
+ * `open()` succeeds — i.e. the instant the file is KNOWN to exist on disk —
117
+ * BEFORE any content byte is written. Lets a caller (`writeInstallFiles`)
118
+ * record its rollback "created fresh, safe to unlink" candidate at the
119
+ * earliest correct moment: without this, a write that later fails (e.g.
120
+ * ENOSPC) after `open()` already created the file left a 0-byte/partial
121
+ * orphan that rollback never knew to remove, since the caller previously
122
+ * only recorded it AFTER this whole function resolved.
123
+ * @throws {ExclusiveCreateRaceError} if the destination already exists
124
+ * @throws {SymlinkError} technically unreachable (O_EXCL fails before O_NOFOLLOW
125
+ * would matter) but kept out of the public contract intentionally — this
126
+ * function never follows anything, by construction.
127
+ */
128
+ export declare function safeCreateFile(filePath: string, content: string | Buffer, options?: WriteFileOptions | string, onCreated?: (identity?: {
129
+ dev: number;
130
+ ino: number;
131
+ }) => void): Promise<void>;
63
132
  //# sourceMappingURL=safe-fs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safe-fs.d.ts","sourceRoot":"","sources":["../../../src/utils/safe-fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAA;AAW1C;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAgB,QAAQ,EAAE,MAAM,CAAA;gBACpB,QAAQ,EAAE,MAAM;CAK7B;AAED;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAgB,QAAQ,EAAE,MAAM,CAAA;gBACpB,QAAQ,EAAE,MAAM;CAK7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAClC,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
1
+ {"version":3,"file":"safe-fs.d.ts","sourceRoot":"","sources":["../../../src/utils/safe-fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAA;AAE1C;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,QAE4B,CAAA;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAAM,GAAG,IAAW,GAC7B,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAgB,QAAQ,EAAE,MAAM,CAAA;gBACpB,QAAQ,EAAE,MAAM;CAK7B;AAED;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAgB,QAAQ,EAAE,MAAM,CAAA;gBACpB,QAAQ,EAAE,MAAM;CAK7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAClC,OAAO,CAAC,IAAI,CAAC,CA6Cf;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,SAAgB,QAAQ,EAAE,MAAM,CAAA;gBACpB,QAAQ,EAAE,MAAM;CAK7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,EACnC,SAAS,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,GAC5D,OAAO,CAAC,IAAI,CAAC,CA0Cf"}
@@ -20,9 +20,40 @@ import { open, lstat, constants } from 'fs/promises';
20
20
  * O_NOFOLLOW: refuse to open symlinks
21
21
  * Prefer runtime constant from fs.constants, fall back to platform-specific values
22
22
  * if unavailable (e.g., Windows where O_NOFOLLOW is not defined).
23
+ *
24
+ * Exported (SMI-6529 F2) so other symlink-safe fd-based writers — e.g.
25
+ * writeInstallFiles' own rollback restore — share this exact platform
26
+ * -fallback resolution instead of a second, driftable copy.
23
27
  */
24
- const O_NOFOLLOW = constants.O_NOFOLLOW ??
28
+ export const O_NOFOLLOW = constants.O_NOFOLLOW ??
25
29
  (process.platform === 'darwin' ? 0x100 : 0x20000);
30
+ /**
31
+ * SMI-6529 N3 (round 4): `FileHandle.write()` is permitted by POSIX (and
32
+ * measured live — 2048/10000 bytes under `RLIMIT_FSIZE`) to write FEWER
33
+ * bytes than requested WITHOUT throwing. Every fd-based writer in this
34
+ * module family used a single unchecked `await fd.write(data)` call, so a
35
+ * short write silently truncated content with no error — for
36
+ * `restoreSnapshots()` specifically, that meant a "successful" restore that
37
+ * left the file smaller than the true original with no `InstallRestoreError`
38
+ * ever raised. Loops until every byte is accounted for, or throws if a call
39
+ * makes zero progress (a short write that returns 0 can otherwise spin
40
+ * forever).
41
+ *
42
+ * @param position - Explicit starting file offset, or `null` to write
43
+ * sequentially from the handle's own current position (which auto-advances
44
+ * after each underlying `write()` call, short or not).
45
+ */
46
+ export async function writeFullBuffer(fd, data, position = null) {
47
+ let written = 0;
48
+ while (written < data.length) {
49
+ const pos = position === null ? null : position + written;
50
+ const { bytesWritten } = await fd.write(data, written, data.length - written, pos);
51
+ if (bytesWritten <= 0) {
52
+ throw new Error(`Short write: made no progress after ${written}/${data.length} bytes written`);
53
+ }
54
+ written += bytesWritten;
55
+ }
56
+ }
26
57
  /**
27
58
  * Error thrown when a symlink is detected at a write destination.
28
59
  */
@@ -110,7 +141,87 @@ export async function safeWriteFile(filePath, content, options) {
110
141
  : typeof content === 'string'
111
142
  ? Buffer.from(content, 'utf-8')
112
143
  : content;
113
- await fd.write(data);
144
+ await writeFullBuffer(fd, data);
145
+ }
146
+ finally {
147
+ await fd?.close();
148
+ }
149
+ }
150
+ /**
151
+ * Error thrown when `safeCreateFile`'s exclusive create finds the destination
152
+ * already occupied — created by something else between the caller's own
153
+ * pre-write classification and this call (a race, not a symlink/hardlink).
154
+ * SMI-6529 L14(b).
155
+ */
156
+ export class ExclusiveCreateRaceError extends Error {
157
+ filePath;
158
+ constructor(filePath) {
159
+ super(`Target appeared during install (race): ${filePath}`);
160
+ this.name = 'ExclusiveCreateRaceError';
161
+ this.filePath = filePath;
162
+ }
163
+ }
164
+ /**
165
+ * SMI-6529 L14(b): create a NEW file exclusively — refuses (throws
166
+ * {@link ExclusiveCreateRaceError}) if anything already exists at `filePath`,
167
+ * symlink or otherwise, instead of silently truncating/overwriting it. Only
168
+ * appropriate for a write target the caller has just classified as `absent`
169
+ * (`classifyPreWrite` in skill-installation.io.rollback.ts) — the atomicity
170
+ * of `O_CREAT|O_EXCL` means "did this call create the file" is knowable
171
+ * without needing `safeWriteFile`'s "record BEFORE writing" hedge (F1, round
172
+ * 1), which exists specifically to survive `O_TRUNC`'s non-atomicity on an
173
+ * OVERWRITE — a case this function is never used for.
174
+ *
175
+ * @param filePath - Absolute path to create (must not already exist)
176
+ * @param content - File content (string or Buffer)
177
+ * @param options - Optional fs.writeFile options (encoding, mode) or encoding string
178
+ * @param onCreated - SMI-6529 N9 (round 4): fired the MOMENT the exclusive
179
+ * `open()` succeeds — i.e. the instant the file is KNOWN to exist on disk —
180
+ * BEFORE any content byte is written. Lets a caller (`writeInstallFiles`)
181
+ * record its rollback "created fresh, safe to unlink" candidate at the
182
+ * earliest correct moment: without this, a write that later fails (e.g.
183
+ * ENOSPC) after `open()` already created the file left a 0-byte/partial
184
+ * orphan that rollback never knew to remove, since the caller previously
185
+ * only recorded it AFTER this whole function resolved.
186
+ * @throws {ExclusiveCreateRaceError} if the destination already exists
187
+ * @throws {SymlinkError} technically unreachable (O_EXCL fails before O_NOFOLLOW
188
+ * would matter) but kept out of the public contract intentionally — this
189
+ * function never follows anything, by construction.
190
+ */
191
+ export async function safeCreateFile(filePath, content, options, onCreated) {
192
+ const mode = typeof options === 'string' ? 0o644 : (options?.mode ?? 0o644);
193
+ const encoding = typeof options === 'string'
194
+ ? options
195
+ : (options?.encoding ?? undefined);
196
+ let fd;
197
+ try {
198
+ fd = await open(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | O_NOFOLLOW, mode);
199
+ // SMI-6529 R5 (round 5): pass the new file's (dev, ino) so a rollback can
200
+ // confirm the path still names THIS file before unlinking it, and never
201
+ // removes a file another process renamed over ours in between. If the
202
+ // stat fails, the identity is omitted and the caller falls back to
203
+ // unlinking by path.
204
+ let identity;
205
+ try {
206
+ const created = await fd.stat();
207
+ identity = { dev: created.dev, ino: created.ino };
208
+ }
209
+ catch {
210
+ identity = undefined;
211
+ }
212
+ onCreated?.(identity);
213
+ const data = typeof content === 'string' && encoding
214
+ ? Buffer.from(content, encoding)
215
+ : typeof content === 'string'
216
+ ? Buffer.from(content, 'utf-8')
217
+ : content;
218
+ await writeFullBuffer(fd, data);
219
+ }
220
+ catch (error) {
221
+ if (error.code === 'EEXIST') {
222
+ throw new ExclusiveCreateRaceError(filePath);
223
+ }
224
+ throw error;
114
225
  }
115
226
  finally {
116
227
  await fd?.close();