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