yam-flow 2.7.0 → 2.8.0
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/AGENTS.md +1 -0
- package/CHANGELOG.md +29 -0
- package/COMMANDS.md +42 -5
- package/README.md +40 -8
- package/ROADMAP.md +17 -6
- package/dist/bin/yam.js +441 -70
- package/dist/lib/external-updates.d.ts +9 -0
- package/dist/lib/external-updates.js +232 -4
- package/dist/lib/next-step.d.ts +61 -0
- package/dist/lib/next-step.js +340 -0
- package/dist/lib/release-auth-readiness.d.ts +81 -0
- package/dist/lib/release-auth-readiness.js +106 -0
- package/dist/lib/release-registry-status.d.ts +18 -0
- package/dist/lib/release-registry-status.js +37 -0
- package/dist/lib/scout-receipt.d.ts +89 -0
- package/dist/lib/scout-receipt.js +542 -0
- package/dist/lib/skill-installation.d.ts +30 -0
- package/dist/lib/skill-installation.js +470 -82
- package/dist/lib/trust-kernel.d.ts +2 -0
- package/dist/lib/trust-kernel.js +21 -2
- package/package.json +22 -3
- package/references/final-report.md +15 -1
- package/references/next-step.md +67 -0
- package/references/scout-wide-scan.md +16 -0
- package/references/study-note.md +4 -2
- package/references/tool-trust-layer.md +2 -0
- package/skills/deep/SKILL.md +2 -0
- package/skills/mission/SKILL.md +2 -0
- package/skills/quick/SKILL.md +1 -0
- package/skills/scout/SKILL.md +19 -9
- package/skills/ueye/SKILL.md +1 -0
- package/src/bin/yam.ts +445 -68
- package/src/lib/external-updates.ts +262 -4
- package/src/lib/next-step.ts +397 -0
- package/src/lib/release-auth-readiness.ts +148 -0
- package/src/lib/release-registry-status.ts +55 -0
- package/src/lib/scout-receipt.ts +593 -0
- package/src/lib/skill-installation.ts +542 -87
- package/src/lib/trust-kernel.ts +25 -2
- package/yam.manifest.json +4 -3
|
@@ -7,6 +7,7 @@ export const INSTALL_RECEIPT_NAME = '.yam-flow-install-receipt.json';
|
|
|
7
7
|
export const INSTALL_LOCK_NAME = '.yam-flow-install.lock';
|
|
8
8
|
export const INSTALL_RECEIPT_SCHEMA = 'yam.install-receipt.v1';
|
|
9
9
|
export const INSTALL_TRANSACTION_PREFIX = '.yam-flow-install-';
|
|
10
|
+
const TRANSACTION_OWNER_MARKER = '.yam-flow-transaction-owner';
|
|
10
11
|
function message(error) {
|
|
11
12
|
return error instanceof Error ? error.message : String(error);
|
|
12
13
|
}
|
|
@@ -106,31 +107,60 @@ function readOnlyNoFollowFlags() {
|
|
|
106
107
|
: 0;
|
|
107
108
|
return fsConstants.O_RDONLY | noFollow;
|
|
108
109
|
}
|
|
110
|
+
async function confirmWindowsPathForHandle(target, opened, before, label) {
|
|
111
|
+
if (before.birthtimeNs <= 0n) {
|
|
112
|
+
throw new Error(`${label} requires a positive birthtime on Windows: ${target}`);
|
|
113
|
+
}
|
|
114
|
+
let pathHandle;
|
|
115
|
+
try {
|
|
116
|
+
pathHandle = await fsp.open(target, fsConstants.O_RDONLY);
|
|
117
|
+
const pathOpened = await pathHandle.stat({ bigint: true });
|
|
118
|
+
const confirmed = await fsp.lstat(target, { bigint: true });
|
|
119
|
+
if (!opened.isFile()
|
|
120
|
+
|| !pathOpened.isFile()
|
|
121
|
+
|| pathOpened.dev !== opened.dev
|
|
122
|
+
|| pathOpened.ino !== opened.ino
|
|
123
|
+
|| confirmed.isSymbolicLink()
|
|
124
|
+
|| !confirmed.isFile()
|
|
125
|
+
|| confirmed.dev !== before.dev
|
|
126
|
+
|| confirmed.ino !== before.ino
|
|
127
|
+
|| confirmed.birthtimeNs !== before.birthtimeNs) {
|
|
128
|
+
throw new Error(`${label} changed identity while confirming Windows path ownership: ${target}`);
|
|
129
|
+
}
|
|
130
|
+
return confirmed;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (pathHandle)
|
|
134
|
+
await pathHandle.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
109
137
|
async function captureRegularFile(file, boundary = path.dirname(file)) {
|
|
110
138
|
const parents = await captureRegularParentPath(boundary, file, 'regular file read');
|
|
111
|
-
const before = await fsp.lstat(file);
|
|
139
|
+
const before = await fsp.lstat(file, { bigint: true });
|
|
112
140
|
if (!before.isFile() || before.isSymbolicLink())
|
|
113
141
|
throw new Error(`expected a regular file: ${file}`);
|
|
114
142
|
const handle = await fsp.open(file, readOnlyNoFollowFlags());
|
|
115
143
|
try {
|
|
116
|
-
const opened = await handle.stat();
|
|
144
|
+
const opened = await handle.stat({ bigint: true });
|
|
117
145
|
if (!opened.isFile())
|
|
118
146
|
throw new Error(`expected a regular file: ${file}`);
|
|
119
147
|
const bytes = await handle.readFile();
|
|
120
|
-
const afterOpened = await handle.stat();
|
|
121
|
-
const after =
|
|
148
|
+
const afterOpened = await handle.stat({ bigint: true });
|
|
149
|
+
const after = process.platform === 'win32'
|
|
150
|
+
? await confirmWindowsPathForHandle(file, opened, before, 'regular file')
|
|
151
|
+
: await fsp.lstat(file, { bigint: true });
|
|
122
152
|
await revalidateRegularParentPath(parents, 'regular file read');
|
|
123
153
|
if (!after.isFile()
|
|
124
154
|
|| after.isSymbolicLink()
|
|
125
|
-
|| before.dev !== opened.dev
|
|
126
|
-
|| before.ino !== opened.ino
|
|
127
155
|
|| afterOpened.dev !== opened.dev
|
|
128
156
|
|| afterOpened.ino !== opened.ino
|
|
129
|
-
||
|
|
130
|
-
|
|
157
|
+
|| (process.platform !== 'win32' && (before.dev !== opened.dev
|
|
158
|
+
|| before.ino !== opened.ino
|
|
159
|
+
|| after.dev !== opened.dev
|
|
160
|
+
|| after.ino !== opened.ino))) {
|
|
131
161
|
throw new Error(`regular file changed identity while being captured: ${file}`);
|
|
132
162
|
}
|
|
133
|
-
return { bytes, mode: opened.mode &
|
|
163
|
+
return { bytes, mode: Number(opened.mode & 511n) };
|
|
134
164
|
}
|
|
135
165
|
finally {
|
|
136
166
|
await handle.close();
|
|
@@ -148,20 +178,23 @@ async function copyRegularFile(source, target, sourceBoundary, targetBoundary) {
|
|
|
148
178
|
const handle = await fsp.open(target, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, sourceFile.mode);
|
|
149
179
|
try {
|
|
150
180
|
await handle.writeFile(sourceFile.bytes);
|
|
151
|
-
const opened = await handle.stat();
|
|
181
|
+
const opened = await handle.stat({ bigint: true });
|
|
152
182
|
if (!opened.isFile())
|
|
153
183
|
throw new Error(`install copy target must be a regular file: ${target}`);
|
|
154
184
|
const openedIdentity = { dev: opened.dev, ino: opened.ino };
|
|
155
|
-
const afterOpened = await handle.stat();
|
|
185
|
+
const afterOpened = await handle.stat({ bigint: true });
|
|
156
186
|
if (afterOpened.dev !== openedIdentity.dev || afterOpened.ino !== openedIdentity.ino) {
|
|
157
187
|
throw new Error(`install copy target changed descriptor identity: ${target}`);
|
|
158
188
|
}
|
|
159
189
|
await revalidateRegularParentPath(targetParents, 'install copy target');
|
|
160
|
-
const
|
|
190
|
+
const beforeInstalled = await fsp.lstat(target, { bigint: true });
|
|
191
|
+
const installed = process.platform === 'win32'
|
|
192
|
+
? await confirmWindowsPathForHandle(target, opened, beforeInstalled, 'install copy target')
|
|
193
|
+
: beforeInstalled;
|
|
161
194
|
if (!installed.isFile()
|
|
162
195
|
|| installed.isSymbolicLink()
|
|
163
|
-
|| installed.dev !== openedIdentity.dev
|
|
164
|
-
|
|
196
|
+
|| (process.platform !== 'win32' && (installed.dev !== openedIdentity.dev
|
|
197
|
+
|| installed.ino !== openedIdentity.ino))) {
|
|
165
198
|
throw new Error(`install copy target changed path identity: ${target}`);
|
|
166
199
|
}
|
|
167
200
|
}
|
|
@@ -232,8 +265,8 @@ async function expectedInstallManifest(sourceRoot, skills) {
|
|
|
232
265
|
const skillFile = path.join(sourceRoot, 'skills', skill, 'SKILL.md');
|
|
233
266
|
const skillBytes = await readRegularFileBytes(skillFile, sourceRoot);
|
|
234
267
|
const skillText = skillBytes.toString('utf8');
|
|
235
|
-
const frontmatter = skillText.match(/^---\n([\s\S]*?)\n
|
|
236
|
-
const name = frontmatter?.[1].match(/^name:\s*([^\n]+)\s*$/m)?.[1]
|
|
268
|
+
const frontmatter = skillText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
269
|
+
const name = frontmatter?.[1].match(/^name:\s*([^\r\n]+)\s*$/m)?.[1]
|
|
237
270
|
?.trim()
|
|
238
271
|
.replace(/^["']|["']$/g, '');
|
|
239
272
|
if (name !== skill) {
|
|
@@ -279,7 +312,7 @@ async function copyTree(source, target, sourceBoundary = source, targetBoundary
|
|
|
279
312
|
await revalidateRegularParentPath(sourceIdentity, 'install source tree');
|
|
280
313
|
await revalidateRegularParentPath(targetIdentity, 'install target tree');
|
|
281
314
|
}
|
|
282
|
-
async function moveRegularDirectory(source, target, sourceBoundary, targetBoundary, label, onMoved) {
|
|
315
|
+
async function moveRegularDirectory(source, target, sourceBoundary, targetBoundary, label, onMoved, identityOwner) {
|
|
283
316
|
const sourceParents = await captureRegularParentPath(sourceBoundary, source, `${label} source`);
|
|
284
317
|
const sourceStat = await fsp.lstat(source);
|
|
285
318
|
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
@@ -288,19 +321,35 @@ async function moveRegularDirectory(source, target, sourceBoundary, targetBounda
|
|
|
288
321
|
const targetParents = await captureRegularParentPath(targetBoundary, target, `${label} target`);
|
|
289
322
|
if (await exists(target))
|
|
290
323
|
throw new Error(`${label} target unexpectedly exists: ${target}`);
|
|
291
|
-
await
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
324
|
+
const recorded = identityOwner ? await captureRecordedEntry(source, 'directory', label) : undefined;
|
|
325
|
+
let identityTransferred = false;
|
|
326
|
+
try {
|
|
327
|
+
await fsp.rename(source, target);
|
|
328
|
+
if (!recorded)
|
|
329
|
+
onMoved?.();
|
|
330
|
+
if (recorded) {
|
|
331
|
+
recorded.path = target;
|
|
332
|
+
identityOwner?.push(recorded);
|
|
333
|
+
identityTransferred = true;
|
|
334
|
+
}
|
|
335
|
+
await revalidateRegularParentPath(sourceParents, `${label} source`);
|
|
336
|
+
await revalidateRegularParentPath(targetParents, `${label} target`);
|
|
337
|
+
const targetStat = await fsp.lstat(target);
|
|
338
|
+
if (!targetStat.isDirectory()
|
|
339
|
+
|| targetStat.isSymbolicLink()
|
|
340
|
+
|| targetStat.dev !== sourceStat.dev
|
|
341
|
+
|| targetStat.ino !== sourceStat.ino) {
|
|
342
|
+
throw new Error(`${label} directory changed identity while being moved: ${target}`);
|
|
343
|
+
}
|
|
344
|
+
return recorded;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (recorded && !identityTransferred)
|
|
348
|
+
await closeRecordedEntry(recorded);
|
|
349
|
+
throw error;
|
|
301
350
|
}
|
|
302
351
|
}
|
|
303
|
-
async function moveRegularFile(source, target, sourceBoundary, targetBoundary, label, onMoved) {
|
|
352
|
+
async function moveRegularFile(source, target, sourceBoundary, targetBoundary, label, onMoved, identityOwner) {
|
|
304
353
|
const sourceParents = await captureRegularParentPath(sourceBoundary, source, `${label} source`);
|
|
305
354
|
const sourceStat = await fsp.lstat(source);
|
|
306
355
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
@@ -309,16 +358,32 @@ async function moveRegularFile(source, target, sourceBoundary, targetBoundary, l
|
|
|
309
358
|
const targetParents = await captureRegularParentPath(targetBoundary, target, `${label} target`);
|
|
310
359
|
if (await exists(target))
|
|
311
360
|
throw new Error(`${label} target unexpectedly exists: ${target}`);
|
|
312
|
-
await
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
361
|
+
const recorded = identityOwner ? await captureRecordedEntry(source, 'file', label) : undefined;
|
|
362
|
+
let identityTransferred = false;
|
|
363
|
+
try {
|
|
364
|
+
await fsp.rename(source, target);
|
|
365
|
+
if (!recorded)
|
|
366
|
+
onMoved?.();
|
|
367
|
+
if (recorded) {
|
|
368
|
+
recorded.path = target;
|
|
369
|
+
identityOwner?.push(recorded);
|
|
370
|
+
identityTransferred = true;
|
|
371
|
+
}
|
|
372
|
+
await revalidateRegularParentPath(sourceParents, `${label} source`);
|
|
373
|
+
await revalidateRegularParentPath(targetParents, `${label} target`);
|
|
374
|
+
const targetStat = await fsp.lstat(target);
|
|
375
|
+
if (!targetStat.isFile()
|
|
376
|
+
|| targetStat.isSymbolicLink()
|
|
377
|
+
|| targetStat.dev !== sourceStat.dev
|
|
378
|
+
|| targetStat.ino !== sourceStat.ino) {
|
|
379
|
+
throw new Error(`${label} file changed identity while being moved: ${target}`);
|
|
380
|
+
}
|
|
381
|
+
return recorded;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (recorded && !identityTransferred)
|
|
385
|
+
await closeRecordedEntry(recorded);
|
|
386
|
+
throw error;
|
|
322
387
|
}
|
|
323
388
|
}
|
|
324
389
|
function manifestDigest(files) {
|
|
@@ -776,28 +841,26 @@ async function acquireInstallLock(destination) {
|
|
|
776
841
|
}
|
|
777
842
|
let lockIdentity;
|
|
778
843
|
try {
|
|
779
|
-
const
|
|
844
|
+
const lockContents = JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }, null, 2);
|
|
845
|
+
await handle.writeFile(lockContents);
|
|
846
|
+
const opened = await handle.stat({ bigint: true });
|
|
780
847
|
if (!opened.isFile())
|
|
781
848
|
throw new Error(`install lock must be a regular file: ${lockPath}`);
|
|
782
|
-
lockIdentity =
|
|
783
|
-
|
|
849
|
+
lockIdentity = await captureLockIdentity(lockPath, opened);
|
|
850
|
+
lockIdentity.contentsSha256 = createHash('sha256').update(lockContents).digest('hex');
|
|
784
851
|
await revalidateRegularParentPath(destinationIdentity, 'skill destination');
|
|
785
|
-
const current = await fsp.lstat(lockPath);
|
|
852
|
+
const current = await fsp.lstat(lockPath, { bigint: true });
|
|
786
853
|
if (current.isSymbolicLink()
|
|
787
854
|
|| !current.isFile()
|
|
788
855
|
|| current.dev !== lockIdentity.dev
|
|
789
|
-
|| current.ino !== lockIdentity.ino
|
|
856
|
+
|| current.ino !== lockIdentity.ino
|
|
857
|
+
|| (lockIdentity.birthtimeNs !== undefined && current.birthtimeNs !== lockIdentity.birthtimeNs)) {
|
|
790
858
|
throw new Error(`install lock changed identity while being acquired: ${lockPath}`);
|
|
791
859
|
}
|
|
792
860
|
}
|
|
793
861
|
catch (error) {
|
|
794
862
|
const cleanupErrors = [];
|
|
795
|
-
|
|
796
|
-
await handle.close();
|
|
797
|
-
}
|
|
798
|
-
catch (closeError) {
|
|
799
|
-
cleanupErrors.push(message(closeError));
|
|
800
|
-
}
|
|
863
|
+
cleanupErrors.push(...await closeFileHandle(handle, lockPath));
|
|
801
864
|
try {
|
|
802
865
|
await revalidateRegularParentPath(destinationIdentity, 'skill destination');
|
|
803
866
|
if (!lockIdentity)
|
|
@@ -807,6 +870,12 @@ async function acquireInstallLock(destination) {
|
|
|
807
870
|
catch (cleanupError) {
|
|
808
871
|
cleanupErrors.push(message(cleanupError));
|
|
809
872
|
}
|
|
873
|
+
finally {
|
|
874
|
+
const closeErrors = lockIdentity
|
|
875
|
+
? await closeRecordedEntries([lockIdentity])
|
|
876
|
+
: [];
|
|
877
|
+
cleanupErrors.push(...closeErrors);
|
|
878
|
+
}
|
|
810
879
|
if (cleanupErrors.length) {
|
|
811
880
|
throw new Error(`${message(error)}. Install lock cleanup failed: ${cleanupErrors.join('; ')}`);
|
|
812
881
|
}
|
|
@@ -858,32 +927,340 @@ async function removeInstalledEntries(entries) {
|
|
|
858
927
|
}
|
|
859
928
|
return errors;
|
|
860
929
|
}
|
|
861
|
-
async function captureRecordedEntry(target, kind, label) {
|
|
862
|
-
const
|
|
930
|
+
async function captureRecordedEntry(target, kind, label, ownershipMarker) {
|
|
931
|
+
const noFollow = process.platform !== 'win32' && typeof fsConstants.O_NOFOLLOW === 'number'
|
|
932
|
+
? fsConstants.O_NOFOLLOW
|
|
933
|
+
: 0;
|
|
934
|
+
const directoryOnly = kind === 'directory'
|
|
935
|
+
&& process.platform !== 'win32'
|
|
936
|
+
&& typeof fsConstants.O_DIRECTORY === 'number'
|
|
937
|
+
? fsConstants.O_DIRECTORY
|
|
938
|
+
: 0;
|
|
939
|
+
let handle;
|
|
940
|
+
try {
|
|
941
|
+
if (process.platform !== 'win32') {
|
|
942
|
+
handle = await fsp.open(target, fsConstants.O_RDONLY | noFollow | directoryOnly);
|
|
943
|
+
}
|
|
944
|
+
if (handle)
|
|
945
|
+
return await captureRecordedEntryFromHandle(target, kind, label, handle);
|
|
946
|
+
const stat = await fsp.lstat(target, { bigint: true });
|
|
947
|
+
const typeMatches = kind === 'directory'
|
|
948
|
+
? stat.isDirectory() && !stat.isSymbolicLink()
|
|
949
|
+
: stat.isFile() && !stat.isSymbolicLink();
|
|
950
|
+
if (!typeMatches || stat.birthtimeNs <= 0n) {
|
|
951
|
+
throw new Error(`${label} must be one stable regular ${kind} with a positive birthtime: ${target}`);
|
|
952
|
+
}
|
|
953
|
+
const entry = {
|
|
954
|
+
path: target,
|
|
955
|
+
dev: stat.dev,
|
|
956
|
+
ino: stat.ino,
|
|
957
|
+
kind,
|
|
958
|
+
birthtimeNs: stat.birthtimeNs,
|
|
959
|
+
ownershipMarker
|
|
960
|
+
};
|
|
961
|
+
if (kind === 'file') {
|
|
962
|
+
const bytes = await readRegularFileBytes(target, path.dirname(target));
|
|
963
|
+
entry.contentsSha256 = createHash('sha256').update(bytes).digest('hex');
|
|
964
|
+
}
|
|
965
|
+
else if (!ownershipMarker) {
|
|
966
|
+
entry.treeDigest = manifestDigest(await collectTreeManifest(target));
|
|
967
|
+
}
|
|
968
|
+
return entry;
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
if (handle)
|
|
972
|
+
await handle.close().catch(() => { });
|
|
973
|
+
throw error;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function captureRecordedEntryFromHandle(target, kind, label, handle) {
|
|
977
|
+
const opened = await handle.stat({ bigint: true });
|
|
978
|
+
const current = await fsp.lstat(target, { bigint: true });
|
|
863
979
|
const typeMatches = kind === 'directory'
|
|
864
|
-
?
|
|
865
|
-
:
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
980
|
+
? opened.isDirectory() && current.isDirectory() && !current.isSymbolicLink()
|
|
981
|
+
: opened.isFile() && current.isFile() && !current.isSymbolicLink();
|
|
982
|
+
const birthtimeAvailable = opened.birthtimeNs > 0n && current.birthtimeNs > 0n;
|
|
983
|
+
if (!typeMatches
|
|
984
|
+
|| opened.dev !== current.dev
|
|
985
|
+
|| opened.ino !== current.ino
|
|
986
|
+
|| (birthtimeAvailable && opened.birthtimeNs !== current.birthtimeNs)) {
|
|
987
|
+
throw new Error(`${label} must be one stable regular ${kind}: ${target}`);
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
path: target,
|
|
991
|
+
dev: opened.dev,
|
|
992
|
+
ino: opened.ino,
|
|
993
|
+
kind,
|
|
994
|
+
birthtimeNs: birthtimeAvailable ? opened.birthtimeNs : undefined,
|
|
995
|
+
handle
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
async function captureLockIdentity(target, created) {
|
|
999
|
+
const current = await fsp.lstat(target, { bigint: true });
|
|
1000
|
+
if (!created.isFile()
|
|
1001
|
+
|| current.isSymbolicLink()
|
|
1002
|
+
|| !current.isFile()
|
|
1003
|
+
|| (process.platform !== 'win32' && (created.dev !== current.dev
|
|
1004
|
+
|| created.ino !== current.ino))) {
|
|
1005
|
+
throw new Error(`install lock changed identity after exclusive creation: ${target}`);
|
|
1006
|
+
}
|
|
1007
|
+
let pinHandle;
|
|
1008
|
+
try {
|
|
1009
|
+
if (process.platform !== 'win32') {
|
|
1010
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
1011
|
+
pinHandle = await fsp.open(target, fsConstants.O_RDONLY | noFollow);
|
|
1012
|
+
const pinned = await pinHandle.stat({ bigint: true });
|
|
1013
|
+
if (!pinned.isFile()
|
|
1014
|
+
|| pinned.dev !== created.dev
|
|
1015
|
+
|| pinned.ino !== created.ino) {
|
|
1016
|
+
throw new Error(`install lock pin descriptor does not match exclusive creation: ${target}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
const confirmed = await confirmWindowsPathForHandle(target, created, current, 'install lock');
|
|
1021
|
+
return {
|
|
1022
|
+
path: target,
|
|
1023
|
+
dev: confirmed.dev,
|
|
1024
|
+
ino: confirmed.ino,
|
|
1025
|
+
kind: 'file',
|
|
1026
|
+
birthtimeNs: confirmed.birthtimeNs
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
path: target,
|
|
1031
|
+
dev: created.dev,
|
|
1032
|
+
ino: created.ino,
|
|
1033
|
+
kind: 'file',
|
|
1034
|
+
handle: pinHandle
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
if (pinHandle)
|
|
1039
|
+
await pinHandle.close().catch(() => { });
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
869
1042
|
}
|
|
870
1043
|
async function removeRecordedEntry(entry, label) {
|
|
871
|
-
let current;
|
|
872
1044
|
try {
|
|
873
|
-
current
|
|
1045
|
+
let current;
|
|
1046
|
+
try {
|
|
1047
|
+
current = await fsp.lstat(entry.path, { bigint: true });
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
if (error.code === 'ENOENT')
|
|
1051
|
+
return;
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
const opened = entry.handle ? await entry.handle.stat({ bigint: true }) : undefined;
|
|
1055
|
+
const typeMatches = entry.kind === 'directory'
|
|
1056
|
+
? current.isDirectory() && !current.isSymbolicLink()
|
|
1057
|
+
: current.isFile() && !current.isSymbolicLink();
|
|
1058
|
+
if (!typeMatches
|
|
1059
|
+
|| current.dev !== entry.dev
|
|
1060
|
+
|| current.ino !== entry.ino
|
|
1061
|
+
|| (entry.birthtimeNs !== undefined && current.birthtimeNs !== entry.birthtimeNs)
|
|
1062
|
+
|| (opened && (opened.dev !== current.dev
|
|
1063
|
+
|| opened.ino !== current.ino
|
|
1064
|
+
|| (entry.birthtimeNs !== undefined && opened.birthtimeNs !== current.birthtimeNs)))) {
|
|
1065
|
+
throw new Error(`${label} identity mismatch; preserved current path: ${entry.path}`);
|
|
1066
|
+
}
|
|
1067
|
+
if (entry.contentsSha256) {
|
|
1068
|
+
try {
|
|
1069
|
+
const bytes = await readRegularFileBytes(entry.path, path.dirname(entry.path));
|
|
1070
|
+
const currentSha256 = createHash('sha256').update(bytes).digest('hex');
|
|
1071
|
+
if (currentSha256 !== entry.contentsSha256)
|
|
1072
|
+
throw new Error('content digest changed');
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
throw new Error(`${label} identity mismatch (content: ${message(error)}); preserved current path: ${entry.path}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (entry.treeDigest) {
|
|
1079
|
+
try {
|
|
1080
|
+
const currentTreeDigest = manifestDigest(await collectTreeManifest(entry.path));
|
|
1081
|
+
if (currentTreeDigest !== entry.treeDigest)
|
|
1082
|
+
throw new Error('tree digest changed');
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
throw new Error(`${label} identity mismatch (tree: ${message(error)}); preserved current path: ${entry.path}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (entry.ownershipMarker) {
|
|
1089
|
+
try {
|
|
1090
|
+
const markerPath = path.join(entry.path, entry.ownershipMarker.name);
|
|
1091
|
+
const markerBytes = await readRegularFileBytes(markerPath, entry.path);
|
|
1092
|
+
const markerSha256 = createHash('sha256').update(markerBytes).digest('hex');
|
|
1093
|
+
if (markerSha256 !== entry.ownershipMarker.sha256)
|
|
1094
|
+
throw new Error('ownership marker digest changed');
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
throw new Error(`${label} identity mismatch (ownership marker: ${message(error)}); preserved current path: ${entry.path}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
await fsp.rm(entry.path, { recursive: entry.kind === 'directory', force: entry.kind === 'file' });
|
|
1101
|
+
}
|
|
1102
|
+
finally {
|
|
1103
|
+
await closeRecordedEntry(entry);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function captureTransactionRoot(target, label) {
|
|
1107
|
+
const markerContents = `${randomUUID()}\n`;
|
|
1108
|
+
const markerPath = path.join(target, TRANSACTION_OWNER_MARKER);
|
|
1109
|
+
await fsp.writeFile(markerPath, markerContents, { flag: 'wx', mode: 0o600 });
|
|
1110
|
+
return captureRecordedEntry(target, 'directory', label, {
|
|
1111
|
+
name: TRANSACTION_OWNER_MARKER,
|
|
1112
|
+
sha256: createHash('sha256').update(markerContents).digest('hex')
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
async function closeRecordedEntry(entry) {
|
|
1116
|
+
const handle = entry.handle;
|
|
1117
|
+
entry.handle = undefined;
|
|
1118
|
+
if (handle)
|
|
1119
|
+
await handle.close();
|
|
1120
|
+
}
|
|
1121
|
+
async function closeRecordedEntries(entries) {
|
|
1122
|
+
const errors = [];
|
|
1123
|
+
for (const entry of entries) {
|
|
1124
|
+
if (!entry)
|
|
1125
|
+
continue;
|
|
1126
|
+
try {
|
|
1127
|
+
await closeRecordedEntry(entry);
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
errors.push(`${entry.path}: ${message(error)}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return errors;
|
|
1134
|
+
}
|
|
1135
|
+
async function closeFileHandle(handle, pathLabel) {
|
|
1136
|
+
try {
|
|
1137
|
+
await handle.close();
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
return [`${pathLabel}: ${message(error)}`];
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
export async function planSkillSetInstallation(options) {
|
|
1145
|
+
const sourceRoot = path.resolve(options.sourceRoot);
|
|
1146
|
+
const destination = safeRoot(options.destination, 'skill destination');
|
|
1147
|
+
const codexMirror = options.codexMirror ? safeRoot(options.codexMirror, 'Codex mirror') : '';
|
|
1148
|
+
const skills = unique(options.skills);
|
|
1149
|
+
const cleanupCandidates = unique([...(options.legacySkills || []), ...(options.retiredSkills || [])]);
|
|
1150
|
+
const replaceSkills = unique(options.replaceSkills || []);
|
|
1151
|
+
validateManagedNames([...skills, ...cleanupCandidates, ...replaceSkills]);
|
|
1152
|
+
const expected = await expectedInstallManifest(sourceRoot, skills);
|
|
1153
|
+
const sourceDigest = manifestDigest(expected);
|
|
1154
|
+
const receiptPath = path.join(destination, INSTALL_RECEIPT_NAME);
|
|
1155
|
+
const operations = [];
|
|
1156
|
+
const blockers = await nonMutatingInstallGateBlockers(destination);
|
|
1157
|
+
let ownership;
|
|
1158
|
+
try {
|
|
1159
|
+
if (blockers.length)
|
|
1160
|
+
throw new Error(blockers[0]);
|
|
1161
|
+
ownership = await preflightInstallOwnership(destination, options.packageName, skills, cleanupCandidates, replaceSkills);
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
if (!blockers.length)
|
|
1165
|
+
blockers.push(message(error));
|
|
1166
|
+
}
|
|
1167
|
+
if (ownership) {
|
|
1168
|
+
const replaceNames = new Set(ownership.replaceNames);
|
|
1169
|
+
for (const skill of skills) {
|
|
1170
|
+
const target = path.join(destination, skill);
|
|
1171
|
+
const existing = await exists(target);
|
|
1172
|
+
operations.push({
|
|
1173
|
+
action: !existing
|
|
1174
|
+
? 'create'
|
|
1175
|
+
: replaceSkills.includes(skill)
|
|
1176
|
+
? 'replace_explicitly_authorized'
|
|
1177
|
+
: 'replace_managed',
|
|
1178
|
+
skill,
|
|
1179
|
+
path: target,
|
|
1180
|
+
reason: !existing
|
|
1181
|
+
? 'active skill is not installed'
|
|
1182
|
+
: replaceSkills.includes(skill)
|
|
1183
|
+
? 'operator supplied an explicit per-skill replacement override'
|
|
1184
|
+
: 'existing skill ownership and complete content integrity match the yam receipt'
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
for (const skill of cleanupCandidates) {
|
|
1188
|
+
if (!replaceNames.has(skill) || skills.includes(skill))
|
|
1189
|
+
continue;
|
|
1190
|
+
operations.push({
|
|
1191
|
+
action: 'remove_retired',
|
|
1192
|
+
skill,
|
|
1193
|
+
path: path.join(destination, skill),
|
|
1194
|
+
reason: 'retired or legacy skill is still proven yam-owned by the current receipt'
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
operations.push({
|
|
1198
|
+
action: 'write_receipt',
|
|
1199
|
+
skill: '',
|
|
1200
|
+
path: receiptPath,
|
|
1201
|
+
reason: ownership.receipt ? 'replace the verified yam ownership receipt' : 'create the yam ownership receipt'
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
const preservedPaths = codexMirror && codexMirror !== destination ? [codexMirror] : [];
|
|
1205
|
+
const replaceNames = new Set(ownership?.replaceNames || []);
|
|
1206
|
+
for (const skill of cleanupCandidates) {
|
|
1207
|
+
if (replaceNames.has(skill) || !await exists(path.join(destination, skill)))
|
|
1208
|
+
continue;
|
|
1209
|
+
preservedPaths.push(path.join(destination, skill));
|
|
1210
|
+
}
|
|
1211
|
+
preservedPaths.sort(compareStableText);
|
|
1212
|
+
const planBasis = {
|
|
1213
|
+
package: { name: options.packageName, version: options.version },
|
|
1214
|
+
source: { root: sourceRoot, digest: sourceDigest, file_count: expected.length },
|
|
1215
|
+
destination,
|
|
1216
|
+
receipt_path: receiptPath,
|
|
1217
|
+
operations,
|
|
1218
|
+
preserved_paths: preservedPaths,
|
|
1219
|
+
blockers
|
|
1220
|
+
};
|
|
1221
|
+
const planDigest = createHash('sha256').update(JSON.stringify(planBasis)).digest('hex');
|
|
1222
|
+
const ready = blockers.length === 0;
|
|
1223
|
+
return {
|
|
1224
|
+
schema: 'yam.install-plan.v1',
|
|
1225
|
+
generated_at: (options.now || (() => new Date()))().toISOString(),
|
|
1226
|
+
mutation_authorized: false,
|
|
1227
|
+
...planBasis,
|
|
1228
|
+
ready,
|
|
1229
|
+
truth_status: ready ? 'verified' : 'blocked',
|
|
1230
|
+
plan_digest: planDigest,
|
|
1231
|
+
next_action: ready
|
|
1232
|
+
? 'review this non-mutating plan, then run `yam install` without --dry-run to authorize the transaction'
|
|
1233
|
+
: 'resolve the listed lock, recovery, destination, or ownership blocker without deleting unreviewed state, then regenerate the plan'
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async function nonMutatingInstallGateBlockers(destination) {
|
|
1237
|
+
let stat;
|
|
1238
|
+
try {
|
|
1239
|
+
stat = await fsp.lstat(destination);
|
|
874
1240
|
}
|
|
875
1241
|
catch (error) {
|
|
876
1242
|
if (error.code === 'ENOENT')
|
|
877
|
-
return;
|
|
878
|
-
|
|
1243
|
+
return [];
|
|
1244
|
+
return [`skill destination cannot be inspected: ${message(error)}`];
|
|
1245
|
+
}
|
|
1246
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
1247
|
+
return [`skill destination must be a regular physical directory: ${destination}`];
|
|
1248
|
+
}
|
|
1249
|
+
const blockers = [];
|
|
1250
|
+
const lockPath = path.join(destination, INSTALL_LOCK_NAME);
|
|
1251
|
+
if (await exists(lockPath)) {
|
|
1252
|
+
blockers.push(`another yam install may be active; lock exists at ${lockPath}`);
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
const recoveryArtifacts = await findInstallRecoveryArtifacts(destination);
|
|
1256
|
+
if (recoveryArtifacts.length) {
|
|
1257
|
+
blockers.push(`unfinished yam install transaction requires inspection: ${recoveryArtifacts.join(', ')}`);
|
|
1258
|
+
}
|
|
879
1259
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
: current.isFile() && !current.isSymbolicLink();
|
|
883
|
-
if (!typeMatches || current.dev !== entry.dev || current.ino !== entry.ino) {
|
|
884
|
-
throw new Error(`${label} identity mismatch; preserved current path: ${entry.path}`);
|
|
1260
|
+
catch (error) {
|
|
1261
|
+
blockers.push(`install recovery state cannot be inspected: ${message(error)}`);
|
|
885
1262
|
}
|
|
886
|
-
|
|
1263
|
+
return blockers;
|
|
887
1264
|
}
|
|
888
1265
|
export async function installSkillSetTransactional(options) {
|
|
889
1266
|
const sourceRoot = path.resolve(options.sourceRoot);
|
|
@@ -910,13 +1287,14 @@ export async function installSkillSetTransactional(options) {
|
|
|
910
1287
|
const expected = await expectedInstallManifest(sourceRoot, skills);
|
|
911
1288
|
const transactionId = options.transactionId || randomUUID();
|
|
912
1289
|
const transactionRoot = await fsp.mkdtemp(path.join(destination, '.yam-flow-install-'));
|
|
913
|
-
const transactionEntry = await
|
|
1290
|
+
const transactionEntry = await captureTransactionRoot(transactionRoot, 'install transaction root');
|
|
914
1291
|
const stagedRoot = path.join(transactionRoot, 'staged');
|
|
915
1292
|
const backupRoot = path.join(transactionRoot, 'backup');
|
|
916
1293
|
const stagedReceiptPath = path.join(transactionRoot, 'receipt.json');
|
|
917
1294
|
const destinationReceiptPath = path.join(destination, INSTALL_RECEIPT_NAME);
|
|
918
1295
|
const receiptBackupPath = path.join(transactionRoot, 'receipt.backup');
|
|
919
1296
|
const installedEntries = [];
|
|
1297
|
+
const receiptInstalledEntries = [];
|
|
920
1298
|
let destinationBackups = [];
|
|
921
1299
|
let receiptBackedUp = false;
|
|
922
1300
|
let receiptInstalled;
|
|
@@ -965,11 +1343,16 @@ export async function installSkillSetTransactional(options) {
|
|
|
965
1343
|
for (const skill of skills) {
|
|
966
1344
|
await revalidateRegularParentPath(lock.destinationIdentity, 'skill destination');
|
|
967
1345
|
const target = path.join(destination, skill);
|
|
968
|
-
await moveRegularDirectory(path.join(stagedRoot, skill), target, stagedRoot, destination, 'staged skill install',
|
|
1346
|
+
const installedEntry = await moveRegularDirectory(path.join(stagedRoot, skill), target, stagedRoot, destination, 'staged skill install', undefined, installedEntries);
|
|
1347
|
+
if (!installedEntry)
|
|
1348
|
+
throw new Error(`staged skill install identity was not recorded: ${target}`);
|
|
969
1349
|
await options.failpoint?.({ phase: 'skill-installed', skill });
|
|
970
1350
|
}
|
|
971
1351
|
await revalidateRegularParentPath(lock.destinationIdentity, 'skill destination');
|
|
972
|
-
await moveRegularFile(stagedReceiptPath, destinationReceiptPath, transactionRoot, destination, 'staged install receipt',
|
|
1352
|
+
receiptInstalled = await moveRegularFile(stagedReceiptPath, destinationReceiptPath, transactionRoot, destination, 'staged install receipt', undefined, receiptInstalledEntries);
|
|
1353
|
+
if (!receiptInstalled) {
|
|
1354
|
+
throw new Error(`staged install receipt identity was not recorded: ${destinationReceiptPath}`);
|
|
1355
|
+
}
|
|
973
1356
|
await options.failpoint?.({ phase: 'receipt-installed' });
|
|
974
1357
|
await options.failpoint?.({ phase: 'before-verify' });
|
|
975
1358
|
await revalidateRegularParentPath(lock.destinationIdentity, 'skill destination');
|
|
@@ -983,6 +1366,10 @@ export async function installSkillSetTransactional(options) {
|
|
|
983
1366
|
});
|
|
984
1367
|
if (!inspection.ok)
|
|
985
1368
|
throw new Error(`post-install verification failed: ${inspection.issues.join('; ')}`);
|
|
1369
|
+
const ownershipHandleErrors = await closeRecordedEntries([receiptInstalled, ...installedEntries]);
|
|
1370
|
+
if (ownershipHandleErrors.length) {
|
|
1371
|
+
cleanupWarnings.push(`verified install identity handle cleanup failed: ${ownershipHandleErrors.join('; ')}`);
|
|
1372
|
+
}
|
|
986
1373
|
try {
|
|
987
1374
|
await removeRecordedEntry(transactionEntry, 'install transaction cleanup');
|
|
988
1375
|
}
|
|
@@ -999,6 +1386,7 @@ export async function installSkillSetTransactional(options) {
|
|
|
999
1386
|
}
|
|
1000
1387
|
catch (error) {
|
|
1001
1388
|
const rollbackErrors = [];
|
|
1389
|
+
receiptInstalled ||= receiptInstalledEntries[0];
|
|
1002
1390
|
let destinationStillOwned = true;
|
|
1003
1391
|
try {
|
|
1004
1392
|
await revalidateRegularParentPath(lock.destinationIdentity, 'skill destination');
|
|
@@ -1037,6 +1425,8 @@ export async function installSkillSetTransactional(options) {
|
|
|
1037
1425
|
}
|
|
1038
1426
|
throw new Error(`yam install failed; previous installation state restored: ${message(error)}`);
|
|
1039
1427
|
}
|
|
1428
|
+
const retainedHandleErrors = await closeRecordedEntries([transactionEntry, receiptInstalled, ...installedEntries]);
|
|
1429
|
+
rollbackErrors.push(...retainedHandleErrors.map((item) => `identity handle cleanup failed: ${item}`));
|
|
1040
1430
|
throw new Error(`yam install failed and rollback is incomplete: ${message(error)}. `
|
|
1041
1431
|
+ `Recovery artifacts were kept at ${transactionRoot}. `
|
|
1042
1432
|
+ `Rollback errors: ${rollbackErrors.join('; ')}`);
|
|
@@ -1046,12 +1436,7 @@ export async function installSkillSetTransactional(options) {
|
|
|
1046
1436
|
primaryError = error;
|
|
1047
1437
|
}
|
|
1048
1438
|
const lockCleanupErrors = [];
|
|
1049
|
-
|
|
1050
|
-
await lock.handle.close();
|
|
1051
|
-
}
|
|
1052
|
-
catch (error) {
|
|
1053
|
-
lockCleanupErrors.push(message(error));
|
|
1054
|
-
}
|
|
1439
|
+
lockCleanupErrors.push(...await closeFileHandle(lock.handle, lock.lockPath));
|
|
1055
1440
|
try {
|
|
1056
1441
|
await revalidateRegularParentPath(lock.destinationIdentity, 'skill destination');
|
|
1057
1442
|
await removeRecordedEntry(lock.lockIdentity, 'install lock cleanup');
|
|
@@ -1059,6 +1444,9 @@ export async function installSkillSetTransactional(options) {
|
|
|
1059
1444
|
catch (error) {
|
|
1060
1445
|
lockCleanupErrors.push(message(error));
|
|
1061
1446
|
}
|
|
1447
|
+
finally {
|
|
1448
|
+
lockCleanupErrors.push(...await closeRecordedEntries([lock.lockIdentity]));
|
|
1449
|
+
}
|
|
1062
1450
|
if (lockCleanupErrors.length) {
|
|
1063
1451
|
const warning = `install lock cleanup failed at ${lock.lockPath}: ${lockCleanupErrors.join('; ')}`;
|
|
1064
1452
|
if (primaryError)
|
|
@@ -1095,7 +1483,7 @@ export async function uninstallSkillSetSafely(options) {
|
|
|
1095
1483
|
}
|
|
1096
1484
|
await preflightSafeUninstallOwnership(destination, options.packageName, skills);
|
|
1097
1485
|
const transactionRoot = await fsp.mkdtemp(path.join(destination, '.yam-flow-install-uninstall-'));
|
|
1098
|
-
const transactionEntry = await
|
|
1486
|
+
const transactionEntry = await captureTransactionRoot(transactionRoot, 'uninstall transaction root');
|
|
1099
1487
|
const backupRoot = path.join(transactionRoot, 'backup');
|
|
1100
1488
|
const receiptPath = path.join(destination, INSTALL_RECEIPT_NAME);
|
|
1101
1489
|
const receiptBackup = path.join(transactionRoot, INSTALL_RECEIPT_NAME);
|
|
@@ -1170,6 +1558,8 @@ export async function uninstallSkillSetSafely(options) {
|
|
|
1170
1558
|
}
|
|
1171
1559
|
throw new Error(`yam uninstall failed; previous installation state restored: ${message(error)}`);
|
|
1172
1560
|
}
|
|
1561
|
+
const retainedHandleErrors = await closeRecordedEntries([transactionEntry]);
|
|
1562
|
+
rollbackErrors.push(...retainedHandleErrors.map((item) => `identity handle cleanup failed: ${item}`));
|
|
1173
1563
|
throw new Error(`yam uninstall failed and rollback is incomplete: ${message(error)}. `
|
|
1174
1564
|
+ `Recovery artifacts were kept at ${transactionRoot}. Rollback errors: ${rollbackErrors.join('; ')}`);
|
|
1175
1565
|
}
|
|
@@ -1178,12 +1568,7 @@ export async function uninstallSkillSetSafely(options) {
|
|
|
1178
1568
|
primaryError = error;
|
|
1179
1569
|
}
|
|
1180
1570
|
const lockCleanupErrors = [];
|
|
1181
|
-
|
|
1182
|
-
await lock.handle.close();
|
|
1183
|
-
}
|
|
1184
|
-
catch (error) {
|
|
1185
|
-
lockCleanupErrors.push(message(error));
|
|
1186
|
-
}
|
|
1571
|
+
lockCleanupErrors.push(...await closeFileHandle(lock.handle, lock.lockPath));
|
|
1187
1572
|
try {
|
|
1188
1573
|
await revalidateRegularParentPath(lock.destinationIdentity, 'skill destination');
|
|
1189
1574
|
await removeRecordedEntry(lock.lockIdentity, 'install lock cleanup');
|
|
@@ -1191,6 +1576,9 @@ export async function uninstallSkillSetSafely(options) {
|
|
|
1191
1576
|
catch (error) {
|
|
1192
1577
|
lockCleanupErrors.push(message(error));
|
|
1193
1578
|
}
|
|
1579
|
+
finally {
|
|
1580
|
+
lockCleanupErrors.push(...await closeRecordedEntries([lock.lockIdentity]));
|
|
1581
|
+
}
|
|
1194
1582
|
if (lockCleanupErrors.length) {
|
|
1195
1583
|
const warning = `install lock cleanup failed at ${lock.lockPath}: ${lockCleanupErrors.join('; ')}`;
|
|
1196
1584
|
if (primaryError)
|