@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.
- package/CHANGELOG.md +168 -0
- package/README.md +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/config/owned-lock.acquire.d.ts +8 -0
- package/dist/src/config/owned-lock.acquire.d.ts.map +1 -1
- package/dist/src/config/owned-lock.acquire.js +19 -4
- package/dist/src/config/owned-lock.acquire.js.map +1 -1
- package/dist/src/config/owned-lock.claim.d.ts +4 -2
- package/dist/src/config/owned-lock.claim.d.ts.map +1 -1
- package/dist/src/config/owned-lock.claim.js +42 -8
- package/dist/src/config/owned-lock.claim.js.map +1 -1
- package/dist/src/config/owned-lock.d.ts.map +1 -1
- package/dist/src/config/owned-lock.js +1 -0
- package/dist/src/config/owned-lock.js.map +1 -1
- package/dist/src/config/owned-lock.test.js +126 -2
- package/dist/src/config/owned-lock.test.js.map +1 -1
- package/dist/src/config/owned-lock.types.d.ts +9 -0
- package/dist/src/config/owned-lock.types.d.ts.map +1 -1
- package/dist/src/db/createDatabase.d.ts.map +1 -1
- package/dist/src/db/createDatabase.js +0 -2
- package/dist/src/db/createDatabase.js.map +1 -1
- package/dist/src/exports/services.install.d.ts +1 -0
- package/dist/src/exports/services.install.d.ts.map +1 -1
- package/dist/src/exports/services.install.js +5 -0
- package/dist/src/exports/services.install.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/install/fan-out.cleanup.d.ts +37 -0
- package/dist/src/install/fan-out.cleanup.d.ts.map +1 -0
- package/dist/src/install/fan-out.cleanup.js +75 -0
- package/dist/src/install/fan-out.cleanup.js.map +1 -0
- package/dist/src/install/fan-out.d.ts +41 -31
- package/dist/src/install/fan-out.d.ts.map +1 -1
- package/dist/src/install/fan-out.js +213 -112
- package/dist/src/install/fan-out.js.map +1 -1
- package/dist/src/install/fan-out.leftovers.d.ts +60 -0
- package/dist/src/install/fan-out.leftovers.d.ts.map +1 -0
- package/dist/src/install/fan-out.leftovers.js +251 -0
- package/dist/src/install/fan-out.leftovers.js.map +1 -0
- package/dist/src/install/fan-out.locks.d.ts +12 -0
- package/dist/src/install/fan-out.locks.d.ts.map +1 -0
- package/dist/src/install/fan-out.locks.js +114 -0
- package/dist/src/install/fan-out.locks.js.map +1 -0
- package/dist/src/install/fan-out.manifest.d.ts +64 -0
- package/dist/src/install/fan-out.manifest.d.ts.map +1 -0
- package/dist/src/install/fan-out.manifest.js +130 -0
- package/dist/src/install/fan-out.manifest.js.map +1 -0
- package/dist/src/install/fan-out.overwrite.d.ts +51 -0
- package/dist/src/install/fan-out.overwrite.d.ts.map +1 -0
- package/dist/src/install/fan-out.overwrite.js +343 -0
- package/dist/src/install/fan-out.overwrite.js.map +1 -0
- package/dist/src/install/remove-if-same.d.ts +58 -0
- package/dist/src/install/remove-if-same.d.ts.map +1 -0
- package/dist/src/install/remove-if-same.js +222 -0
- package/dist/src/install/remove-if-same.js.map +1 -0
- package/dist/src/provenance/backfill.d.ts.map +1 -1
- package/dist/src/provenance/backfill.js +15 -2
- package/dist/src/provenance/backfill.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.scanners.js +8 -1
- package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.value-gate.d.ts +22 -2
- package/dist/src/security/scanner/SecurityScanner.value-gate.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.value-gate.js +143 -6
- package/dist/src/security/scanner/SecurityScanner.value-gate.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.weak-passwords.d.ts +2 -2
- package/dist/src/security/scanner/SecurityScanner.weak-passwords.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.weak-passwords.js +185 -190
- package/dist/src/security/scanner/SecurityScanner.weak-passwords.js.map +1 -1
- package/dist/src/security/scanner/patterns.d.ts +47 -2
- package/dist/src/security/scanner/patterns.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.js +48 -3
- package/dist/src/security/scanner/patterns.js.map +1 -1
- package/dist/src/security/scanner/patterns.sensitive-path.d.ts +31 -3
- package/dist/src/security/scanner/patterns.sensitive-path.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.sensitive-path.js +59 -4
- package/dist/src/security/scanner/patterns.sensitive-path.js.map +1 -1
- package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -1
- package/dist/src/services/agent-pack/prompt-source.js +3 -2
- package/dist/src/services/agent-pack/prompt-source.js.map +1 -1
- package/dist/src/services/skill-installation.content.d.ts +7 -2
- package/dist/src/services/skill-installation.content.d.ts.map +1 -1
- package/dist/src/services/skill-installation.content.js +75 -28
- package/dist/src/services/skill-installation.content.js.map +1 -1
- package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
- package/dist/src/services/skill-installation.helpers.js +8 -0
- package/dist/src/services/skill-installation.helpers.js.map +1 -1
- package/dist/src/services/skill-installation.io.d.ts +1 -29
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.dirs.d.ts +15 -0
- package/dist/src/services/skill-installation.io.dirs.d.ts.map +1 -0
- package/dist/src/services/skill-installation.io.dirs.js +84 -0
- package/dist/src/services/skill-installation.io.dirs.js.map +1 -0
- package/dist/src/services/skill-installation.io.js +6 -176
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.io.rollback.d.ts +138 -0
- package/dist/src/services/skill-installation.io.rollback.d.ts.map +1 -0
- package/dist/src/services/skill-installation.io.rollback.js +218 -0
- package/dist/src/services/skill-installation.io.rollback.js.map +1 -0
- package/dist/src/services/skill-installation.io.write.d.ts +69 -0
- package/dist/src/services/skill-installation.io.write.d.ts.map +1 -0
- package/dist/src/services/skill-installation.io.write.js +417 -0
- package/dist/src/services/skill-installation.io.write.js.map +1 -0
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.finalize.d.ts +62 -0
- package/dist/src/services/skill-installation.service.finalize.d.ts.map +1 -0
- package/dist/src/services/skill-installation.service.finalize.js +136 -0
- package/dist/src/services/skill-installation.service.finalize.js.map +1 -0
- package/dist/src/services/skill-installation.service.js +66 -80
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/services/skill-installation.target-guard.d.ts +82 -0
- package/dist/src/services/skill-installation.target-guard.d.ts.map +1 -0
- package/dist/src/services/skill-installation.target-guard.js +410 -0
- package/dist/src/services/skill-installation.target-guard.js.map +1 -0
- package/dist/src/services/skill-installation.types.d.ts +13 -1
- package/dist/src/services/skill-installation.types.d.ts.map +1 -1
- package/dist/src/services/skill-installation.types.js.map +1 -1
- package/dist/src/services/skill-installation.uninstall.d.ts +3 -1
- package/dist/src/services/skill-installation.uninstall.d.ts.map +1 -1
- package/dist/src/services/skill-installation.uninstall.helpers.d.ts +59 -0
- package/dist/src/services/skill-installation.uninstall.helpers.d.ts.map +1 -0
- package/dist/src/services/skill-installation.uninstall.helpers.js +124 -0
- package/dist/src/services/skill-installation.uninstall.helpers.js.map +1 -0
- package/dist/src/services/skill-installation.uninstall.js +148 -25
- package/dist/src/services/skill-installation.uninstall.js.map +1 -1
- package/dist/src/utils/safe-fs.d.ts +69 -0
- package/dist/src/utils/safe-fs.d.ts.map +1 -1
- package/dist/src/utils/safe-fs.js +113 -2
- package/dist/src/utils/safe-fs.js.map +1 -1
- package/dist/tests/install/fan-out.test.js +1951 -13
- package/dist/tests/install/fan-out.test.js.map +1 -1
- package/dist/tests/install/remove-if-same.test.d.ts +2 -0
- package/dist/tests/install/remove-if-same.test.d.ts.map +1 -0
- package/dist/tests/install/remove-if-same.test.js +298 -0
- package/dist/tests/install/remove-if-same.test.js.map +1 -0
- package/dist/tests/integration/fan-out-lock-contention.test.d.ts +11 -0
- package/dist/tests/integration/fan-out-lock-contention.test.d.ts.map +1 -0
- package/dist/tests/integration/fan-out-lock-contention.test.js +53 -0
- package/dist/tests/integration/fan-out-lock-contention.test.js.map +1 -0
- package/dist/tests/provenance/backfill.test.js +43 -0
- package/dist/tests/provenance/backfill.test.js.map +1 -1
- package/dist/tests/security/scanner-regression-guard.test.js +33 -5
- package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
- package/dist/tests/security/sensitive-path-adversarial-review.test.js +120 -0
- package/dist/tests/security/sensitive-path-adversarial-review.test.js.map +1 -1
- package/dist/tests/security/sensitive-path-boolean-flag.test.d.ts +2 -0
- package/dist/tests/security/sensitive-path-boolean-flag.test.d.ts.map +1 -0
- package/dist/tests/security/sensitive-path-boolean-flag.test.js +151 -0
- package/dist/tests/security/sensitive-path-boolean-flag.test.js.map +1 -0
- package/dist/tests/security/sensitive-path-fp.test.js +71 -5
- package/dist/tests/security/sensitive-path-fp.test.js.map +1 -1
- package/dist/tests/security/sensitive-path-prefixed-secrets.test.d.ts +2 -0
- package/dist/tests/security/sensitive-path-prefixed-secrets.test.d.ts.map +1 -0
- package/dist/tests/security/sensitive-path-prefixed-secrets.test.js +176 -0
- package/dist/tests/security/sensitive-path-prefixed-secrets.test.js.map +1 -0
- package/dist/tests/security/weak-password-lexicon.test.js +14 -4
- package/dist/tests/security/weak-password-lexicon.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.content.test.js +23 -0
- package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +35 -14
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.io.rollback.test.d.ts +2 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.test.js +984 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.d.ts +2 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.js +148 -0
- package/dist/tests/unit/services/skill-installation.io.rollback.unit.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +19 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.service.finalize.test.d.ts +2 -0
- package/dist/tests/unit/services/skill-installation.service.finalize.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.finalize.test.js +147 -0
- package/dist/tests/unit/services/skill-installation.service.finalize.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.test.js +43 -0
- package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.target-guard.test.d.ts +2 -0
- package/dist/tests/unit/services/skill-installation.target-guard.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.target-guard.test.js +590 -0
- package/dist/tests/unit/services/skill-installation.target-guard.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.uninstall.guard.test.d.ts +2 -0
- package/dist/tests/unit/services/skill-installation.uninstall.guard.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.uninstall.guard.test.js +473 -0
- package/dist/tests/unit/services/skill-installation.uninstall.guard.test.js.map +1 -0
- package/dist/tests/utils/safe-fs.test.js +129 -2
- package/dist/tests/utils/safe-fs.test.js.map +1 -1
- 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"}
|