loadout-ai 0.2.0 → 0.2.2
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 +16 -0
- package/MASTER_PLAN.md +11 -8
- package/dist/src/cli.js +1 -1
- package/dist/src/core/catalog-install.js +23 -2
- package/dist/src/core/install.js +146 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Reconcile Stable and Power to their exact managed skill sets instead of leaving skills from the previous profile active.
|
|
8
|
+
- Preview every managed skill that profile setup will retire and reject an apply if managed state changed after the preview.
|
|
9
|
+
- Snapshot retired skills in the same transaction, preserve unmanaged skills, and refuse to retire locally changed managed content.
|
|
10
|
+
|
|
11
|
+
## 0.2.1 - 2026-07-17
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Make Stable and Power setup safe to rerun when Loadout already owns some target skills.
|
|
16
|
+
- Preserve unmanaged skills and refuse to replace managed skills that changed outside Loadout.
|
|
17
|
+
- Verify every replacement byte and keep mixed existing/new-agent installs rollback-safe.
|
|
18
|
+
|
|
3
19
|
## 0.2.0 - 2026-07-17
|
|
4
20
|
|
|
5
21
|
### Added
|
package/MASTER_PLAN.md
CHANGED
|
@@ -1023,13 +1023,12 @@ universal or permanent truth.
|
|
|
1023
1023
|
`NOASSERTION` records, before distribution.
|
|
1024
1024
|
- [ ] `P12-31 [TERRA]` Publish `loadout-ai` to npm and run clean-machine package tests
|
|
1025
1025
|
from outside the repository on macOS, Windows, Linux, and Node 20/22.
|
|
1026
|
-
- Partial: `loadout-ai@0.
|
|
1027
|
-
repository remained private. Registry
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
installs on Windows/macOS/Linux remain required.
|
|
1026
|
+
- Partial: `loadout-ai@0.2.0` was published publicly on 2026-07-17 while the GitHub
|
|
1027
|
+
repository remained private. Registry version/integrity, a clean external npm
|
|
1028
|
+
install, version/help, 50-record catalog coverage, and the real disposable
|
|
1029
|
+
Superpowers install/rollback demo pass on macOS. Hosted matrix evidence already
|
|
1030
|
+
covers Windows/macOS/Linux and Node 20/22; clean independent-user installs on
|
|
1031
|
+
Windows and Linux remain required.
|
|
1033
1032
|
- [ ] `P12-32 [HUMAN]` Run moderated founder testing on the real Claude and Codex
|
|
1034
1033
|
profiles with snapshots and explicit rollback checkpoints.
|
|
1035
1034
|
- Partial 2026-07-16: the founder ran the published npm CLI against the real macOS
|
|
@@ -1170,7 +1169,7 @@ universal or permanent truth.
|
|
|
1170
1169
|
1,000 on-disk skills at a 1.29-second p95 on macOS/Node 23.
|
|
1171
1170
|
- [ ] `P15-06 [HUMAN]` Approve public repository visibility, complete the six
|
|
1172
1171
|
`NOASSERTION` license decisions, authenticate npm, and publish `loadout-ai`.
|
|
1173
|
-
- Partial 2026-07-
|
|
1172
|
+
- Partial 2026-07-17: npm authentication and the public `loadout-ai@0.2.0`
|
|
1174
1173
|
publication succeeded. Repository visibility remains private at the owner's
|
|
1175
1174
|
request, and the six root-level `NOASSERTION` decisions remain a human release
|
|
1176
1175
|
gate; therefore this combined item is not ticked.
|
|
@@ -1601,6 +1600,10 @@ accident.
|
|
|
1601
1600
|
- Repeat the safe no-key journey with Claude, then collect one Windows and one Linux
|
|
1602
1601
|
external session. Convert every reproducible failure into a regression test before
|
|
1603
1602
|
public-release claims.
|
|
1603
|
+
- Partial 2026-07-17: the registry and clean external macOS install are verified for
|
|
1604
|
+
`0.2.0`; the published CLI loads the complete command surface, reports all 50
|
|
1605
|
+
catalog records, and completes its disposable install/rollback demo. Real founder
|
|
1606
|
+
Stable/Power/Maximum and Claude-profile exercises remain deliberately pending.
|
|
1604
1607
|
- [x] `P17-09 [TERRA implementation, LUNA copy review]` Make Power resilient and
|
|
1605
1608
|
explain the product in beginner language before the `0.2.0` release.
|
|
1606
1609
|
- Power now quarantines a rejected selected skill while retaining safe selected
|
package/dist/src/cli.js
CHANGED
|
@@ -262,7 +262,7 @@ async function runSetup(options) {
|
|
|
262
262
|
reader?.close();
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
-
const LOADOUT_VERSION = "0.2.
|
|
265
|
+
const LOADOUT_VERSION = "0.2.2";
|
|
266
266
|
function durableSchedulerLauncher() {
|
|
267
267
|
return [
|
|
268
268
|
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadEffectiveCatalog } from "./catalog.js";
|
|
2
2
|
import { detectAgents } from "./paths.js";
|
|
3
3
|
import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "./profiles.js";
|
|
4
|
-
import { applySkillLibraryBatch, applySkillInstallBatch, buildSkillPlan, installedAgents, } from "./install.js";
|
|
4
|
+
import { applySkillLibraryBatch, applySkillInstallBatch, buildSkillPlan, installedAgents, planManagedProfileReconciliation, } from "./install.js";
|
|
5
5
|
import { fetchRepositorySnapshot, } from "./source.js";
|
|
6
6
|
import { analyzeInstallPlanSafety, } from "./safety.js";
|
|
7
7
|
import { formatModelApiAccess } from "./access.js";
|
|
@@ -175,6 +175,14 @@ export async function prepareCatalogInstall(selection, options = {}) {
|
|
|
175
175
|
});
|
|
176
176
|
return false;
|
|
177
177
|
});
|
|
178
|
+
const reconciliation = selection.mode === "maximum"
|
|
179
|
+
? {
|
|
180
|
+
obsoleteActivationKeys: [],
|
|
181
|
+
obsoletePackageIds: [],
|
|
182
|
+
obsoleteTargets: [],
|
|
183
|
+
obsoleteUnits: [],
|
|
184
|
+
}
|
|
185
|
+
: await planManagedProfileReconciliation(usableEntries);
|
|
178
186
|
return {
|
|
179
187
|
selection,
|
|
180
188
|
resolution,
|
|
@@ -183,6 +191,7 @@ export async function prepareCatalogInstall(selection, options = {}) {
|
|
|
183
191
|
skipped,
|
|
184
192
|
collisions,
|
|
185
193
|
access: options.access ?? { modelApis: [] },
|
|
194
|
+
reconciliation,
|
|
186
195
|
};
|
|
187
196
|
}
|
|
188
197
|
export function formatPreparedCatalogInstall(prepared) {
|
|
@@ -194,6 +203,7 @@ export function formatPreparedCatalogInstall(prepared) {
|
|
|
194
203
|
const explicit = prepared.skipped.filter((item) => item.kind === "explicit-setup");
|
|
195
204
|
const failures = prepared.skipped.filter((item) => item.kind === "preparation-failed");
|
|
196
205
|
const quarantined = prepared.skipped.filter((item) => item.kind === "quarantined");
|
|
206
|
+
const retired = prepared.reconciliation?.obsoleteUnits ?? [];
|
|
197
207
|
const lines = [
|
|
198
208
|
`Loadout: ${prepared.selection.mode === "maximum" ? "Maximum Library" : prepared.selection.mode === "power" ? "Power Boost" : prepared.selection.mode === "stable" ? "Stable Boost" : "Custom"}`,
|
|
199
209
|
`Detected agents: ${prepared.agents.map((agent) => agent.displayName).join(", ")}`,
|
|
@@ -211,6 +221,11 @@ export function formatPreparedCatalogInstall(prepared) {
|
|
|
211
221
|
lines.push(`Quarantined invalid skill units: ${quarantined.length} (safe siblings remain available)`);
|
|
212
222
|
if (prepared.collisions.length)
|
|
213
223
|
lines.push(`Overlapping skill targets resolved: ${prepared.collisions.length} lower-ranked duplicate directories deferred`);
|
|
224
|
+
if (retired.length) {
|
|
225
|
+
lines.push(`Profile reconciliation: ${retired.length} active managed skill${retired.length === 1 ? "" : "s"} will be retired from the selected agents.`);
|
|
226
|
+
for (const item of retired)
|
|
227
|
+
lines.push(`Retire ${item.packageId}${item.unitId ? `/${item.unitId}` : ""} from ${item.agent}`);
|
|
228
|
+
}
|
|
214
229
|
if (risky.length)
|
|
215
230
|
lines.push(`Additional risk approval required: ${risky.map((entry) => entry.package.id).join(", ")}`);
|
|
216
231
|
for (const warning of prepared.resolution.warnings)
|
|
@@ -230,5 +245,11 @@ export async function applyPreparedCatalogInstall(prepared, options = {}) {
|
|
|
230
245
|
throw new Error(`Additional risk approval is required for: ${risky.map((entry) => entry.package.id).join(", ")}. Review the plan, then use --approve-risk.`);
|
|
231
246
|
return prepared.selection.mode === "maximum"
|
|
232
247
|
? applySkillLibraryBatch(prepared.entries)
|
|
233
|
-
: applySkillInstallBatch(prepared.entries
|
|
248
|
+
: applySkillInstallBatch(prepared.entries, [], {
|
|
249
|
+
replaceManagedTargets: true,
|
|
250
|
+
reconcileManagedTargets: true,
|
|
251
|
+
...(prepared.reconciliation
|
|
252
|
+
? { expectedReconciliation: prepared.reconciliation }
|
|
253
|
+
: {}),
|
|
254
|
+
});
|
|
234
255
|
}
|
package/dist/src/core/install.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { cp, lstat, rm } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, join, posix, relative, win32 } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, posix, relative, win32, } from "node:path";
|
|
4
4
|
import { ensureDirectory, loadoutHome } from "./paths.js";
|
|
5
5
|
import { planAdapterSkillInstall } from "./adapters.js";
|
|
6
6
|
import { applySkillPlan, detectInstallConflicts } from "./skills.js";
|
|
7
|
-
import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, } from "./state.js";
|
|
7
|
+
import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "./state.js";
|
|
8
8
|
import { runMutationTransaction } from "./transaction.js";
|
|
9
9
|
export function installedAgents(agents, requested) {
|
|
10
10
|
const available = agents.filter((agent) => agent.installed);
|
|
@@ -17,6 +17,100 @@ export function installedAgents(agents, requested) {
|
|
|
17
17
|
}
|
|
18
18
|
return selected;
|
|
19
19
|
}
|
|
20
|
+
function relativeHashes(root, files) {
|
|
21
|
+
return files
|
|
22
|
+
.filter((file) => isInside(root, file.path))
|
|
23
|
+
.map((file) => ({
|
|
24
|
+
path: relative(root, file.path),
|
|
25
|
+
sha256: file.sha256,
|
|
26
|
+
}))
|
|
27
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
28
|
+
}
|
|
29
|
+
function isInside(root, path) {
|
|
30
|
+
const child = relative(root, path);
|
|
31
|
+
return child !== "" && !child.startsWith("..") && !isAbsolute(child);
|
|
32
|
+
}
|
|
33
|
+
function activationKey(record) {
|
|
34
|
+
return `${record.packageId}\0${record.agent}\0${record.unitId ?? ""}`;
|
|
35
|
+
}
|
|
36
|
+
async function assertManagedTargetUnchanged(target, owner) {
|
|
37
|
+
const expected = relativeHashes(target, owner?.files ?? []);
|
|
38
|
+
const actual = relativeHashes(target, await hashDirectory(target));
|
|
39
|
+
if (!expected.length || JSON.stringify(actual) !== JSON.stringify(expected))
|
|
40
|
+
throw new Error(`Installation refuses to replace drifted managed skill target: ${target}`);
|
|
41
|
+
}
|
|
42
|
+
export async function planManagedProfileReconciliation(entries) {
|
|
43
|
+
const state = await readInstallState();
|
|
44
|
+
const requestedAgents = new Set(entries.flatMap((entry) => entry.plan.targetAgents));
|
|
45
|
+
const desiredTargets = new Set(entries.flatMap((entry) => entry.plan.files.map((file) => file.target)));
|
|
46
|
+
const obsolete = [];
|
|
47
|
+
for (const activation of state.activations ?? []) {
|
|
48
|
+
if (!requestedAgents.has(activation.agent) ||
|
|
49
|
+
activation.installationState !== "installed" ||
|
|
50
|
+
activation.activationState !== "active")
|
|
51
|
+
continue;
|
|
52
|
+
const staleTargets = activation.targets.filter((target) => !desiredTargets.has(target.activePath));
|
|
53
|
+
if (!staleTargets.length)
|
|
54
|
+
continue;
|
|
55
|
+
if (staleTargets.length !== activation.targets.length)
|
|
56
|
+
throw new Error(`Profile reconciliation refuses a partially selected managed unit: ${activation.packageId}/${activation.unitId ?? "skill"}`);
|
|
57
|
+
const owner = state.installs.find((record) => record.packageId === activation.packageId);
|
|
58
|
+
for (const target of staleTargets)
|
|
59
|
+
await assertManagedTargetUnchanged(target.activePath, owner);
|
|
60
|
+
obsolete.push(activation);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
obsoleteActivationKeys: obsolete.map(activationKey),
|
|
64
|
+
obsoletePackageIds: [...new Set(obsolete.map((item) => item.packageId))],
|
|
65
|
+
obsoleteTargets: [
|
|
66
|
+
...new Set(obsolete.flatMap((item) => item.targets.map((target) => target.activePath))),
|
|
67
|
+
],
|
|
68
|
+
obsoleteUnits: obsolete.map((item) => ({
|
|
69
|
+
packageId: item.packageId,
|
|
70
|
+
agent: item.agent,
|
|
71
|
+
...(item.unitId ? { unitId: item.unitId } : {}),
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function reconciliationSignature(reconciliation) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
keys: [...reconciliation.obsoleteActivationKeys].sort(),
|
|
78
|
+
targets: [...reconciliation.obsoleteTargets].sort(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function recordManagedProfileReconciliation(reconciliation) {
|
|
82
|
+
if (!reconciliation.obsoleteActivationKeys.length)
|
|
83
|
+
return;
|
|
84
|
+
const obsoleteKeys = new Set(reconciliation.obsoleteActivationKeys);
|
|
85
|
+
const affectedPackages = new Set(reconciliation.obsoletePackageIds);
|
|
86
|
+
const state = await readInstallState();
|
|
87
|
+
state.activations = (state.activations ?? []).filter((record) => !obsoleteKeys.has(activationKey(record)));
|
|
88
|
+
state.installs = state.installs.flatMap((install) => {
|
|
89
|
+
if (!affectedPackages.has(install.packageId))
|
|
90
|
+
return [install];
|
|
91
|
+
const remaining = (state.activations ?? []).filter((record) => record.packageId === install.packageId &&
|
|
92
|
+
record.installationState === "installed");
|
|
93
|
+
if (!remaining.length)
|
|
94
|
+
return [];
|
|
95
|
+
const roots = remaining.flatMap((record) => record.targets.map((target) => target.activePath));
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
...install,
|
|
99
|
+
targetAgents: [...new Set(remaining.map((record) => record.agent))],
|
|
100
|
+
files: install.files.filter((file) => roots.some((root) => isInside(root, file.path))),
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
await writeInstallState(state);
|
|
105
|
+
}
|
|
106
|
+
async function assertExactDirectoryCopy(source, target, label) {
|
|
107
|
+
const [expected, actual] = await Promise.all([
|
|
108
|
+
hashDirectory(source).then((files) => relativeHashes(source, files)),
|
|
109
|
+
hashDirectory(target).then((files) => relativeHashes(target, files)),
|
|
110
|
+
]);
|
|
111
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected))
|
|
112
|
+
throw new Error(`${label} for ${target}`);
|
|
113
|
+
}
|
|
20
114
|
async function assertActiveTargetsUnoccupied(plans, options = {}) {
|
|
21
115
|
const occupied = [];
|
|
22
116
|
for (const target of [
|
|
@@ -35,26 +129,36 @@ async function assertActiveTargetsUnoccupied(plans, options = {}) {
|
|
|
35
129
|
}
|
|
36
130
|
if (occupied.length && options.allowManagedReplacement) {
|
|
37
131
|
const state = await readInstallState();
|
|
38
|
-
const allowed = new
|
|
132
|
+
const allowed = new Map();
|
|
133
|
+
for (const plan of plans) {
|
|
39
134
|
const install = state.installs.find((record) => record.packageId === plan.packageId);
|
|
40
135
|
const packageActivations = (state.activations ?? []).filter((record) => record.packageId === plan.packageId);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
136
|
+
if (!install)
|
|
137
|
+
continue;
|
|
138
|
+
const activeTargets = new Set(packageActivations
|
|
139
|
+
.filter((record) => record.installationState === "installed" &&
|
|
140
|
+
record.activationState === "active")
|
|
141
|
+
.flatMap((record) => record.targets.map((target) => target.activePath)));
|
|
46
142
|
const recorded = install.files
|
|
47
143
|
.filter((file) => basename(file.path) === "SKILL.md")
|
|
48
|
-
.map((file) => dirname(file.path))
|
|
144
|
+
.map((file) => dirname(file.path))
|
|
145
|
+
.filter((target) => packageActivations.length === 0 || activeTargets.has(target));
|
|
49
146
|
const legacy = packageActivations.length
|
|
50
147
|
? []
|
|
51
148
|
: plan.files
|
|
52
149
|
.map((file) => file.target)
|
|
53
150
|
.filter((target) => basename(target) === plan.packageId);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
151
|
+
for (const target of [...recorded, ...legacy])
|
|
152
|
+
if (plan.files.some((file) => file.target === target))
|
|
153
|
+
allowed.set(target, install);
|
|
154
|
+
}
|
|
155
|
+
if (occupied.every((target) => allowed.has(target))) {
|
|
156
|
+
for (const target of occupied) {
|
|
157
|
+
const owner = allowed.get(target);
|
|
158
|
+
await assertManagedTargetUnchanged(target, owner);
|
|
159
|
+
}
|
|
57
160
|
return;
|
|
161
|
+
}
|
|
58
162
|
}
|
|
59
163
|
if (occupied.length)
|
|
60
164
|
throw new Error(`Installation refuses ${occupied.length} occupied skill target(s); scan, compare, or adopt them first. First target: ${occupied[0]}`);
|
|
@@ -106,20 +210,7 @@ export async function applySkillInstall(plan, metadata, options = {}) {
|
|
|
106
210
|
await applySkillPlan(freshPlan);
|
|
107
211
|
if (options.replaceManagedTargets)
|
|
108
212
|
for (const file of freshPlan.files) {
|
|
109
|
-
|
|
110
|
-
.map((entry) => ({
|
|
111
|
-
path: relative(file.source, entry.path),
|
|
112
|
-
sha256: entry.sha256,
|
|
113
|
-
}))
|
|
114
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
115
|
-
const actual = (await hashDirectory(file.target))
|
|
116
|
-
.map((entry) => ({
|
|
117
|
-
path: relative(file.target, entry.path),
|
|
118
|
-
sha256: entry.sha256,
|
|
119
|
-
}))
|
|
120
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
121
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected))
|
|
122
|
-
throw new Error(`Exact update copy verification failed for ${file.target}`);
|
|
213
|
+
await assertExactDirectoryCopy(file.source, file.target, "Exact update copy verification failed");
|
|
123
214
|
}
|
|
124
215
|
await recordInstall(freshPlan, snapshot.id, metadata);
|
|
125
216
|
await options.verifyBeforeCommit?.(snapshot.id);
|
|
@@ -127,7 +218,7 @@ export async function applySkillInstall(plan, metadata, options = {}) {
|
|
|
127
218
|
return applied.snapshotId;
|
|
128
219
|
}
|
|
129
220
|
/** Apply all selected packages as one filesystem transaction and one state update. */
|
|
130
|
-
export async function applySkillInstallBatch(entries, extraSnapshotPaths = []) {
|
|
221
|
+
export async function applySkillInstallBatch(entries, extraSnapshotPaths = [], options = {}) {
|
|
131
222
|
if (!entries.length)
|
|
132
223
|
throw new Error("Installation batch is empty");
|
|
133
224
|
const conflicts = detectInstallConflicts(entries.map((entry) => entry.plan));
|
|
@@ -135,7 +226,19 @@ export async function applySkillInstallBatch(entries, extraSnapshotPaths = []) {
|
|
|
135
226
|
if (blocking.length)
|
|
136
227
|
throw new Error(`Installation blocked by conflicts: ${blocking.map((item) => item.message).join("; ")}`);
|
|
137
228
|
const applied = await runMutationTransaction(async () => {
|
|
138
|
-
|
|
229
|
+
const reconciliation = options.reconcileManagedTargets
|
|
230
|
+
? await planManagedProfileReconciliation(entries)
|
|
231
|
+
: {
|
|
232
|
+
obsoleteActivationKeys: [],
|
|
233
|
+
obsoletePackageIds: [],
|
|
234
|
+
obsoleteTargets: [],
|
|
235
|
+
obsoleteUnits: [],
|
|
236
|
+
};
|
|
237
|
+
if (options.expectedReconciliation &&
|
|
238
|
+
reconciliationSignature(reconciliation) !==
|
|
239
|
+
reconciliationSignature(options.expectedReconciliation))
|
|
240
|
+
throw new Error("Profile reconciliation refused because managed state changed after preview; prepare the plan again.");
|
|
241
|
+
await assertActiveTargetsUnoccupied(entries.map((entry) => entry.plan), { allowManagedReplacement: options.replaceManagedTargets });
|
|
139
242
|
for (const entry of entries) {
|
|
140
243
|
entry.plan.conflicts = [
|
|
141
244
|
...(entry.plan.conflicts ?? []),
|
|
@@ -156,13 +259,26 @@ export async function applySkillInstallBatch(entries, extraSnapshotPaths = []) {
|
|
|
156
259
|
...entries.flatMap((entry) => entry.plan.files.map((file) => file.target)),
|
|
157
260
|
installStatePath(),
|
|
158
261
|
...extraSnapshotPaths,
|
|
262
|
+
...reconciliation.obsoleteTargets,
|
|
159
263
|
],
|
|
160
|
-
value: entries,
|
|
264
|
+
value: { entries, reconciliation },
|
|
161
265
|
};
|
|
162
|
-
}, async (freshEntries, snapshot) => {
|
|
266
|
+
}, async ({ entries: freshEntries, reconciliation }, snapshot) => {
|
|
267
|
+
for (const target of reconciliation.obsoleteTargets)
|
|
268
|
+
await rm(target, { recursive: true, force: true });
|
|
269
|
+
if (options.replaceManagedTargets)
|
|
270
|
+
for (const target of [
|
|
271
|
+
...new Set(freshEntries.flatMap((entry) => entry.plan.files.map((file) => file.target))),
|
|
272
|
+
])
|
|
273
|
+
await rm(target, { recursive: true, force: true });
|
|
163
274
|
for (const entry of freshEntries)
|
|
164
275
|
await applySkillPlan(entry.plan);
|
|
276
|
+
if (options.replaceManagedTargets)
|
|
277
|
+
for (const entry of freshEntries)
|
|
278
|
+
for (const file of entry.plan.files)
|
|
279
|
+
await assertExactDirectoryCopy(file.source, file.target, "Exact setup copy verification failed");
|
|
165
280
|
await recordInstallBatch(freshEntries, snapshot.id);
|
|
281
|
+
await recordManagedProfileReconciliation(reconciliation);
|
|
166
282
|
});
|
|
167
283
|
return applied.snapshotId;
|
|
168
284
|
}
|