skill-flow 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -3
- package/README.zh.md +40 -3
- package/dist/adapters/channel-adapters.js +11 -3
- package/dist/adapters/channel-adapters.js.map +1 -1
- package/dist/cli.js +69 -37
- package/dist/cli.js.map +1 -1
- package/dist/domain/types.d.ts +54 -1
- package/dist/services/config-coordinator.d.ts +38 -0
- package/dist/services/config-coordinator.js +81 -0
- package/dist/services/config-coordinator.js.map +1 -0
- package/dist/services/doctor-service.d.ts +2 -0
- package/dist/services/doctor-service.js +62 -0
- package/dist/services/doctor-service.js.map +1 -1
- package/dist/services/inventory-service.d.ts +3 -1
- package/dist/services/inventory-service.js +12 -5
- package/dist/services/inventory-service.js.map +1 -1
- package/dist/services/skill-flow.d.ts +50 -26
- package/dist/services/skill-flow.js +502 -89
- package/dist/services/skill-flow.js.map +1 -1
- package/dist/services/source-service.d.ts +20 -10
- package/dist/services/source-service.js +359 -75
- package/dist/services/source-service.js.map +1 -1
- package/dist/services/workflow-service.d.ts +2 -2
- package/dist/services/workflow-service.js +17 -4
- package/dist/services/workflow-service.js.map +1 -1
- package/dist/services/workspace-bootstrap-service.d.ts +25 -0
- package/dist/services/workspace-bootstrap-service.js +140 -0
- package/dist/services/workspace-bootstrap-service.js.map +1 -0
- package/dist/state/store.d.ts +16 -0
- package/dist/state/store.js +93 -18
- package/dist/state/store.js.map +1 -1
- package/dist/tests/clawhub.test.d.ts +1 -0
- package/dist/tests/clawhub.test.js +63 -0
- package/dist/tests/clawhub.test.js.map +1 -0
- package/dist/tests/cli-utils.test.d.ts +1 -0
- package/dist/tests/cli-utils.test.js +15 -0
- package/dist/tests/cli-utils.test.js.map +1 -0
- package/dist/tests/config-coordinator.test.d.ts +1 -0
- package/dist/tests/config-coordinator.test.js +172 -0
- package/dist/tests/config-coordinator.test.js.map +1 -0
- package/dist/tests/config-integration.test.d.ts +1 -0
- package/dist/tests/config-integration.test.js +238 -0
- package/dist/tests/config-integration.test.js.map +1 -0
- package/dist/tests/config-ui-utils.test.d.ts +1 -0
- package/dist/tests/config-ui-utils.test.js +389 -0
- package/dist/tests/config-ui-utils.test.js.map +1 -0
- package/dist/tests/find-and-naming-utils.test.d.ts +1 -0
- package/dist/tests/find-and-naming-utils.test.js +127 -0
- package/dist/tests/find-and-naming-utils.test.js.map +1 -0
- package/dist/tests/skill-flow.test.js +334 -881
- package/dist/tests/skill-flow.test.js.map +1 -1
- package/dist/tests/source-lifecycle.test.d.ts +1 -0
- package/dist/tests/source-lifecycle.test.js +605 -0
- package/dist/tests/source-lifecycle.test.js.map +1 -0
- package/dist/tests/target-definitions.test.d.ts +1 -0
- package/dist/tests/target-definitions.test.js +51 -0
- package/dist/tests/target-definitions.test.js.map +1 -0
- package/dist/tests/test-helpers.d.ts +18 -0
- package/dist/tests/test-helpers.js +123 -0
- package/dist/tests/test-helpers.js.map +1 -0
- package/dist/tui/config-app.d.ts +147 -24
- package/dist/tui/config-app.js +1081 -335
- package/dist/tui/config-app.js.map +1 -1
- package/dist/tui/find-app.d.ts +1 -1
- package/dist/tui/find-app.js +36 -4
- package/dist/tui/find-app.js.map +1 -1
- package/dist/utils/clawhub.d.ts +3 -0
- package/dist/utils/clawhub.js +32 -3
- package/dist/utils/clawhub.js.map +1 -1
- package/dist/utils/cli.d.ts +1 -0
- package/dist/utils/cli.js +15 -0
- package/dist/utils/cli.js.map +1 -0
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.js +31 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +52 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/naming.d.ts +1 -0
- package/dist/utils/naming.js +7 -1
- package/dist/utils/naming.js.map +1 -1
- package/dist/utils/source-id.js +4 -0
- package/dist/utils/source-id.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,51 +1,92 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { createChannelAdapters } from "../adapters/channel-adapters.js";
|
|
3
4
|
import { StateStore } from "../state/store.js";
|
|
4
|
-
import { ensureDir, hashDirectory, pathExists, readJsonFile, removePath, writeJsonFile } from "../utils/fs.js";
|
|
5
|
+
import { ensureDir, hashDirectory, pathExists, readJsonFile, removePath, writeJsonFile, } from "../utils/fs.js";
|
|
5
6
|
import { getBuiltinGitSources } from "../utils/builtin-git-sources.js";
|
|
6
7
|
import { fetchGitHubSkillPaths } from "../utils/github-catalog.js";
|
|
7
|
-
import { parseGitHubRepo } from "../utils/naming.js";
|
|
8
|
+
import { buildProjectedSkillNameCandidates, parseGitHubRepo, resolveProjectedSkillNames, } from "../utils/naming.js";
|
|
8
9
|
import { fail, ok } from "../utils/result.js";
|
|
9
10
|
import { searchClawHubSkills } from "../utils/clawhub.js";
|
|
10
11
|
import { deriveDisplayName, deriveSourceId } from "../utils/source-id.js";
|
|
11
12
|
import { DeploymentApplier } from "./deployment-applier.js";
|
|
13
|
+
import { ConfigCoordinator } from "./config-coordinator.js";
|
|
12
14
|
import { DeploymentPlanner } from "./deployment-planner.js";
|
|
13
15
|
import { DoctorService } from "./doctor-service.js";
|
|
14
16
|
import { InventoryService } from "./inventory-service.js";
|
|
15
17
|
import { SourceService } from "./source-service.js";
|
|
16
18
|
import { WorkflowService } from "./workflow-service.js";
|
|
19
|
+
import { WorkspaceBootstrapService, } from "./workspace-bootstrap-service.js";
|
|
17
20
|
export class SkillFlowApp {
|
|
18
|
-
store
|
|
19
|
-
inventoryService
|
|
20
|
-
sourceService
|
|
21
|
-
planner
|
|
22
|
-
applier
|
|
23
|
-
doctorService
|
|
24
|
-
workflowService
|
|
21
|
+
store;
|
|
22
|
+
inventoryService;
|
|
23
|
+
sourceService;
|
|
24
|
+
planner;
|
|
25
|
+
applier;
|
|
26
|
+
doctorService;
|
|
27
|
+
workflowService;
|
|
28
|
+
workspaceBootstrapService;
|
|
29
|
+
configCoordinator;
|
|
30
|
+
mutationQueue = Promise.resolve();
|
|
31
|
+
constructor() {
|
|
32
|
+
this.store = new StateStore();
|
|
33
|
+
this.inventoryService = new InventoryService();
|
|
34
|
+
this.sourceService = new SourceService(this.store, this.inventoryService);
|
|
35
|
+
this.planner = new DeploymentPlanner(createChannelAdapters());
|
|
36
|
+
this.applier = new DeploymentApplier();
|
|
37
|
+
this.doctorService = new DoctorService();
|
|
38
|
+
this.workflowService = new WorkflowService();
|
|
39
|
+
this.workspaceBootstrapService = new WorkspaceBootstrapService(this.store);
|
|
40
|
+
this.configCoordinator = new ConfigCoordinator({
|
|
41
|
+
store: this.store,
|
|
42
|
+
doctorService: this.doctorService,
|
|
43
|
+
workflowService: this.workflowService,
|
|
44
|
+
getAvailableTargets: () => this.getAvailableTargets(),
|
|
45
|
+
pruneMissingCheckouts: () => this.pruneMissingCheckoutsImpl(),
|
|
46
|
+
getConfigData: () => this.getConfigDataImpl(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
25
49
|
async addSource(locator, options) {
|
|
26
|
-
|
|
50
|
+
return this.runSerializedMutation(() => this.addSourceImpl(locator, options));
|
|
51
|
+
}
|
|
52
|
+
async addSourceImpl(locator, options) {
|
|
53
|
+
const addOptions = options ?? {};
|
|
54
|
+
const result = await this.sourceService.addSource(locator, addOptions);
|
|
27
55
|
if (!result.ok) {
|
|
28
56
|
return result;
|
|
29
57
|
}
|
|
30
|
-
await this.store.
|
|
31
|
-
const manifest = await this.store.readManifest();
|
|
32
|
-
const lockFile = await this.store.readLock();
|
|
58
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
33
59
|
const source = manifest.sources.find((item) => item.id === result.data.manifest.id);
|
|
34
60
|
if (!source) {
|
|
35
61
|
return result;
|
|
36
62
|
}
|
|
63
|
+
source.selectionMode =
|
|
64
|
+
addOptions.selectionMode ??
|
|
65
|
+
(source.requestedPath ? "partial" : "all");
|
|
66
|
+
const enabledTargets = addOptions.enabledTargets ??
|
|
67
|
+
await this.getAvailableTargets();
|
|
37
68
|
manifest.bindings[source.id] = this.bindingFromDraft({
|
|
38
|
-
enabledTargets
|
|
69
|
+
enabledTargets,
|
|
39
70
|
selectedLeafIds: this.selectLeafIdsForRequestedPath(lockFile.leafInventory.filter((leaf) => leaf.sourceId === source.id), source.requestedPath),
|
|
40
71
|
});
|
|
41
|
-
await this.store.
|
|
42
|
-
|
|
72
|
+
await this.store.writeState(manifest, lockFile);
|
|
73
|
+
if (addOptions.project === false) {
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
const plan = await this.planForAffectedSources(manifest, lockFile, source.id);
|
|
77
|
+
if (!plan.ok) {
|
|
78
|
+
return fail(plan.errors, [...result.warnings, ...plan.warnings]);
|
|
79
|
+
}
|
|
80
|
+
const applied = await this.applier.applyPlan(lockFile, plan.data.actions);
|
|
81
|
+
await this.store.writeState(manifest, lockFile);
|
|
82
|
+
if (!applied.ok) {
|
|
83
|
+
return fail(applied.errors, [...result.warnings, ...plan.warnings, ...applied.warnings]);
|
|
84
|
+
}
|
|
85
|
+
return ok(result.data, [...result.warnings, ...plan.warnings, ...applied.warnings]);
|
|
43
86
|
}
|
|
44
87
|
async findSkills(query) {
|
|
45
|
-
await this.store.
|
|
46
|
-
const
|
|
47
|
-
const lockFile = await this.store.readLock();
|
|
48
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
88
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
89
|
+
const normalizedQuery = this.normalizeSearchQuery(query);
|
|
49
90
|
const warnings = [];
|
|
50
91
|
const localKeys = new Set();
|
|
51
92
|
const candidates = [];
|
|
@@ -90,7 +131,7 @@ export class SkillFlowApp {
|
|
|
90
131
|
}
|
|
91
132
|
}
|
|
92
133
|
try {
|
|
93
|
-
const results = await searchClawHubSkills(
|
|
134
|
+
const results = await searchClawHubSkills(normalizedQuery, 8);
|
|
94
135
|
remoteSearchSucceeded = true;
|
|
95
136
|
for (const result of results) {
|
|
96
137
|
candidates.push({
|
|
@@ -126,35 +167,63 @@ export class SkillFlowApp {
|
|
|
126
167
|
return ok({ candidates }, warnings);
|
|
127
168
|
}
|
|
128
169
|
async listWorkflows() {
|
|
170
|
+
return this.runSerializedMutation(() => this.listWorkflowsImpl());
|
|
171
|
+
}
|
|
172
|
+
async listWorkflowsImpl() {
|
|
173
|
+
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
174
|
+
if (!pruned.ok) {
|
|
175
|
+
return fail(pruned.errors, pruned.warnings);
|
|
176
|
+
}
|
|
129
177
|
const reconciled = await this.sourceService.reconcileInventory(undefined, {
|
|
130
178
|
force: true,
|
|
131
179
|
});
|
|
132
180
|
if (!reconciled.ok) {
|
|
133
181
|
return fail(reconciled.errors, reconciled.warnings);
|
|
134
182
|
}
|
|
135
|
-
await this.store.
|
|
136
|
-
const manifest = await this.store.readManifest();
|
|
137
|
-
const lockFile = await this.store.readLock();
|
|
183
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
138
184
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
139
185
|
return ok({
|
|
140
186
|
summaries: this.workflowService.getSummaries(manifest, lockFile),
|
|
141
|
-
});
|
|
187
|
+
}, pruned.warnings);
|
|
142
188
|
}
|
|
143
189
|
async getConfigData() {
|
|
190
|
+
return this.runSerializedMutation(() => this.getConfigDataImpl());
|
|
191
|
+
}
|
|
192
|
+
async getConfigDataImpl() {
|
|
193
|
+
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
194
|
+
if (!pruned.ok) {
|
|
195
|
+
return fail(pruned.errors, pruned.warnings);
|
|
196
|
+
}
|
|
144
197
|
const reconciled = await this.sourceService.reconcileInventory(undefined, {
|
|
145
198
|
force: true,
|
|
146
199
|
});
|
|
147
200
|
if (!reconciled.ok) {
|
|
148
201
|
return fail(reconciled.errors, reconciled.warnings);
|
|
149
202
|
}
|
|
150
|
-
await this.store.
|
|
151
|
-
const manifest = await this.store.readManifest();
|
|
152
|
-
const lockFile = await this.store.readLock();
|
|
203
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
153
204
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
154
205
|
return ok({
|
|
155
206
|
manifest,
|
|
156
207
|
lockFile,
|
|
157
208
|
summaries: this.workflowService.getSummaries(manifest, lockFile),
|
|
209
|
+
}, pruned.warnings);
|
|
210
|
+
}
|
|
211
|
+
async bootstrapWorkspaceState(onEvent) {
|
|
212
|
+
return this.runSerializedMutation(() => this.bootstrapWorkspaceStateImpl(onEvent));
|
|
213
|
+
}
|
|
214
|
+
async bootstrapWorkspaceStateImpl(onEvent) {
|
|
215
|
+
const boot = await this.configCoordinator.bootstrapWorkspaceState(onEvent);
|
|
216
|
+
if (!boot.ok) {
|
|
217
|
+
return fail(boot.errors, boot.warnings);
|
|
218
|
+
}
|
|
219
|
+
return ok({
|
|
220
|
+
availableTargets: boot.data.availableTargets,
|
|
221
|
+
manifest: boot.data.manifest,
|
|
222
|
+
lockFile: boot.data.lockFile,
|
|
223
|
+
summaries: boot.data.summaries,
|
|
224
|
+
initialDrafts: boot.data.initialDrafts,
|
|
225
|
+
audit: boot.data.audit,
|
|
226
|
+
importedSourceIds: [],
|
|
158
227
|
});
|
|
159
228
|
}
|
|
160
229
|
async getAvailableTargets() {
|
|
@@ -172,9 +241,7 @@ export class SkillFlowApp {
|
|
|
172
241
|
// config TUI state flow:
|
|
173
242
|
// draft -> previewDraft() -> plan only
|
|
174
243
|
// draft -> applyDraft() -> plan + filesystem + manifest/lock writes
|
|
175
|
-
await this.store.
|
|
176
|
-
const manifest = await this.store.readManifest();
|
|
177
|
-
const lockFile = await this.store.readLock();
|
|
244
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
178
245
|
this.normalizeBindings(manifest, lockFile);
|
|
179
246
|
const prepared = this.prepareManifestForDraft(manifest, lockFile, sourceId, draft);
|
|
180
247
|
const plan = await this.planForAffectedSources(prepared.manifest, lockFile, sourceId);
|
|
@@ -184,15 +251,10 @@ export class SkillFlowApp {
|
|
|
184
251
|
return ok({ plan: plan.data, manifest: prepared.manifest, lockFile }, [...prepared.warnings, ...plan.warnings]);
|
|
185
252
|
}
|
|
186
253
|
async applyDraft(sourceId, draft) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return fail(reconciled.errors, reconciled.warnings);
|
|
192
|
-
}
|
|
193
|
-
await this.store.init();
|
|
194
|
-
const manifest = await this.store.readManifest();
|
|
195
|
-
const lockFile = await this.store.readLock();
|
|
254
|
+
return this.runSerializedMutation(() => this.applyDraftImpl(sourceId, draft));
|
|
255
|
+
}
|
|
256
|
+
async applyDraftImpl(sourceId, draft) {
|
|
257
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
196
258
|
this.normalizeBindings(manifest, lockFile);
|
|
197
259
|
const prepared = this.prepareManifestForDraft(manifest, lockFile, sourceId, draft);
|
|
198
260
|
const plan = await this.planForAffectedSources(prepared.manifest, lockFile, sourceId);
|
|
@@ -200,55 +262,143 @@ export class SkillFlowApp {
|
|
|
200
262
|
return fail(plan.errors, [...prepared.warnings, ...plan.warnings]);
|
|
201
263
|
}
|
|
202
264
|
const applyResult = await this.applier.applyPlan(lockFile, plan.data.actions);
|
|
203
|
-
await this.store.
|
|
204
|
-
await this.store.writeLock(lockFile);
|
|
265
|
+
await this.store.writeState(prepared.manifest, lockFile);
|
|
205
266
|
if (!applyResult.ok) {
|
|
206
267
|
return fail(applyResult.errors, [...prepared.warnings, ...plan.warnings, ...applyResult.warnings]);
|
|
207
268
|
}
|
|
208
269
|
return ok({ actions: plan.data.actions, draft: prepared.draft }, [...prepared.warnings, ...plan.warnings, ...applyResult.warnings]);
|
|
209
270
|
}
|
|
210
271
|
async updateSources(sourceIds) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
272
|
+
return this.runSerializedMutation(() => this.updateSourcesImpl(sourceIds));
|
|
273
|
+
}
|
|
274
|
+
async updateSourcesImpl(sourceIds) {
|
|
275
|
+
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
276
|
+
if (!pruned.ok) {
|
|
277
|
+
return fail(pruned.errors, pruned.warnings);
|
|
216
278
|
}
|
|
217
|
-
const
|
|
279
|
+
const requestedIds = sourceIds?.filter((sourceId) => !pruned.data.removedSourceIds.includes(sourceId));
|
|
280
|
+
if (sourceIds?.length && requestedIds?.length === 0) {
|
|
281
|
+
return ok({ updated: [] }, pruned.warnings);
|
|
282
|
+
}
|
|
283
|
+
const updated = await this.sourceService.updateSources(requestedIds);
|
|
218
284
|
if (!updated.ok) {
|
|
219
285
|
return updated;
|
|
220
286
|
}
|
|
221
|
-
const manifest = await this.store.
|
|
222
|
-
|
|
287
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
288
|
+
this.applySourceUpdateResults(manifest, lockFile, updated.data.updated);
|
|
223
289
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
224
|
-
const
|
|
290
|
+
const planSourceIds = manifest.sources
|
|
225
291
|
.map((source) => source.id)
|
|
226
|
-
.filter((id) =>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
await this.applier.applyPlan(lockFile, plan.data.actions);
|
|
292
|
+
.filter((id) => updated.data.updated.some((item) => item.sourceId === id) ||
|
|
293
|
+
this.hasActiveTargets(manifest, id) ||
|
|
294
|
+
lockFile.deployments.some((deployment) => deployment.sourceId === id));
|
|
295
|
+
const planned = await this.planAndApplySources(manifest, lockFile, planSourceIds);
|
|
296
|
+
if (!planned.ok) {
|
|
297
|
+
return fail(planned.errors, [...pruned.warnings, ...updated.warnings, ...planned.warnings]);
|
|
233
298
|
}
|
|
234
|
-
await this.store.
|
|
235
|
-
return updated;
|
|
299
|
+
await this.store.writeState(manifest, lockFile);
|
|
300
|
+
return ok(updated.data, [...pruned.warnings, ...updated.warnings, ...planned.warnings]);
|
|
236
301
|
}
|
|
237
302
|
async doctor() {
|
|
303
|
+
return this.runSerializedMutation(() => this.doctorImpl());
|
|
304
|
+
}
|
|
305
|
+
async doctorImpl() {
|
|
306
|
+
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
307
|
+
if (!pruned.ok) {
|
|
308
|
+
return fail(pruned.errors, pruned.warnings);
|
|
309
|
+
}
|
|
238
310
|
const reconciled = await this.sourceService.reconcileInventory();
|
|
239
311
|
if (!reconciled.ok) {
|
|
240
312
|
return fail(reconciled.errors, reconciled.warnings);
|
|
241
313
|
}
|
|
242
|
-
await this.store.
|
|
243
|
-
const manifest = await this.store.readManifest();
|
|
244
|
-
const lockFile = await this.store.readLock();
|
|
314
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
245
315
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
246
|
-
|
|
316
|
+
const doctor = await this.doctorService.run(manifest, lockFile);
|
|
317
|
+
if (!doctor.ok) {
|
|
318
|
+
return doctor;
|
|
319
|
+
}
|
|
320
|
+
return ok(doctor.data, [...pruned.warnings, ...doctor.warnings]);
|
|
321
|
+
}
|
|
322
|
+
async repairTargets(sourceIds) {
|
|
323
|
+
return this.runSerializedMutation(() => this.repairTargetsImpl(sourceIds));
|
|
324
|
+
}
|
|
325
|
+
async repairTargetsImpl(sourceIds) {
|
|
326
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
327
|
+
this.normalizeBindings(manifest, lockFile);
|
|
328
|
+
const requestedIds = sourceIds?.length
|
|
329
|
+
? sourceIds
|
|
330
|
+
: manifest.sources.map((source) => source.id);
|
|
331
|
+
for (const sourceId of requestedIds) {
|
|
332
|
+
if (!manifest.sources.some((source) => source.id === sourceId)) {
|
|
333
|
+
return fail({
|
|
334
|
+
code: "SOURCE_NOT_FOUND",
|
|
335
|
+
message: `Skills group id '${sourceId}' is not registered.`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const planSourceIds = requestedIds.filter((sourceId) => this.hasActiveTargets(manifest, sourceId) ||
|
|
340
|
+
lockFile.deployments.some((deployment) => deployment.sourceId === sourceId));
|
|
341
|
+
const planned = await this.planAndApplySources(manifest, lockFile, planSourceIds);
|
|
342
|
+
if (!planned.ok) {
|
|
343
|
+
return fail(planned.errors, planned.warnings);
|
|
344
|
+
}
|
|
345
|
+
await this.store.writeState(manifest, lockFile);
|
|
346
|
+
return ok({ actions: planned.data.actions }, planned.warnings);
|
|
347
|
+
}
|
|
348
|
+
async repairSource(sourceIds) {
|
|
349
|
+
return this.runSerializedMutation(() => this.repairSourceImpl(sourceIds));
|
|
350
|
+
}
|
|
351
|
+
async repairSourceImpl(sourceIds) {
|
|
352
|
+
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
353
|
+
if (!pruned.ok) {
|
|
354
|
+
return fail(pruned.errors, pruned.warnings);
|
|
355
|
+
}
|
|
356
|
+
const requestedIds = sourceIds?.filter((sourceId) => !pruned.data.removedSourceIds.includes(sourceId));
|
|
357
|
+
if (sourceIds?.length && requestedIds?.length === 0) {
|
|
358
|
+
return ok({ updated: [] }, pruned.warnings);
|
|
359
|
+
}
|
|
360
|
+
const repaired = await this.sourceService.updateSources(requestedIds);
|
|
361
|
+
if (!repaired.ok) {
|
|
362
|
+
return repaired;
|
|
363
|
+
}
|
|
364
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
365
|
+
this.applySourceUpdateResults(manifest, lockFile, repaired.data.updated);
|
|
366
|
+
await this.persistNormalizedBindings(manifest, lockFile);
|
|
367
|
+
await this.store.writeState(manifest, lockFile);
|
|
368
|
+
return ok(repaired.data, [...pruned.warnings, ...repaired.warnings]);
|
|
369
|
+
}
|
|
370
|
+
async repairState(sourceIds) {
|
|
371
|
+
return this.runSerializedMutation(() => this.repairStateImpl(sourceIds));
|
|
372
|
+
}
|
|
373
|
+
async repairStateImpl(sourceIds) {
|
|
374
|
+
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
375
|
+
if (!pruned.ok) {
|
|
376
|
+
return fail(pruned.errors, pruned.warnings);
|
|
377
|
+
}
|
|
378
|
+
const requestedIds = sourceIds?.filter((sourceId) => !pruned.data.removedSourceIds.includes(sourceId));
|
|
379
|
+
if (sourceIds?.length && requestedIds?.length === 0) {
|
|
380
|
+
return ok({ repairedSourceIds: [], removedDeploymentCount: 0 }, pruned.warnings);
|
|
381
|
+
}
|
|
382
|
+
const reconciled = await this.sourceService.reconcileInventory(requestedIds, {
|
|
383
|
+
force: true,
|
|
384
|
+
});
|
|
385
|
+
if (!reconciled.ok) {
|
|
386
|
+
return fail(reconciled.errors, [...pruned.warnings, ...reconciled.warnings]);
|
|
387
|
+
}
|
|
388
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
389
|
+
await this.persistNormalizedBindings(manifest, lockFile);
|
|
390
|
+
const removedDeploymentCount = await this.rebuildDeploymentState(manifest, lockFile, requestedIds);
|
|
391
|
+
await this.store.writeState(manifest, lockFile);
|
|
392
|
+
return ok({
|
|
393
|
+
repairedSourceIds: reconciled.data.updatedSourceIds,
|
|
394
|
+
removedDeploymentCount,
|
|
395
|
+
}, [...pruned.warnings, ...reconciled.warnings]);
|
|
247
396
|
}
|
|
248
397
|
async uninstall(sourceIds) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
398
|
+
return this.runSerializedMutation(() => this.uninstallImpl(sourceIds));
|
|
399
|
+
}
|
|
400
|
+
async uninstallImpl(sourceIds) {
|
|
401
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
252
402
|
const warnings = [];
|
|
253
403
|
const removedRefs = sourceIds
|
|
254
404
|
.map((sourceId) => manifest.sources.find((source) => source.id === sourceId))
|
|
@@ -259,26 +409,33 @@ export class SkillFlowApp {
|
|
|
259
409
|
if (!(await pathExists(deployment.targetPath))) {
|
|
260
410
|
continue;
|
|
261
411
|
}
|
|
262
|
-
|
|
263
|
-
const stats = await fs.lstat(deployment.targetPath);
|
|
264
|
-
if (stats.isSymbolicLink()) {
|
|
265
|
-
await removePath(deployment.targetPath);
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
warnings.push(`Skipped ${deployment.targetPath} because it no longer looks like a managed symlink.`);
|
|
269
|
-
}
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const currentHash = await hashDirectory(deployment.targetPath);
|
|
273
|
-
if (currentHash === deployment.contentHash) {
|
|
412
|
+
try {
|
|
274
413
|
await removePath(deployment.targetPath);
|
|
275
414
|
}
|
|
276
|
-
|
|
277
|
-
warnings.push(`
|
|
415
|
+
catch (error) {
|
|
416
|
+
warnings.push(`Unable to remove ${deployment.targetPath}: ${String(error)}`);
|
|
278
417
|
}
|
|
279
418
|
}
|
|
280
419
|
}
|
|
281
|
-
|
|
420
|
+
if (warnings.length > 0) {
|
|
421
|
+
return fail({
|
|
422
|
+
code: "GROUP_DELETE_INCOMPLETE",
|
|
423
|
+
message: `Unable to fully delete ${warnings.length} managed path${warnings.length === 1 ? "" : "s"}.`,
|
|
424
|
+
}, warnings.map((message) => ({
|
|
425
|
+
code: "GROUP_DELETE_PATH_FAILED",
|
|
426
|
+
message,
|
|
427
|
+
})));
|
|
428
|
+
}
|
|
429
|
+
let removed;
|
|
430
|
+
try {
|
|
431
|
+
removed = await this.sourceService.removeSource(sourceIds);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return fail({
|
|
435
|
+
code: "GROUP_DELETE_INCOMPLETE",
|
|
436
|
+
message: `Unable to fully delete selected skills groups: ${String(error)}`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
282
439
|
if (!removed.ok) {
|
|
283
440
|
return fail(removed.errors, removed.warnings);
|
|
284
441
|
}
|
|
@@ -294,11 +451,58 @@ export class SkillFlowApp {
|
|
|
294
451
|
}
|
|
295
452
|
return { targets };
|
|
296
453
|
}
|
|
454
|
+
async pruneMissingCheckoutsImpl() {
|
|
455
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
456
|
+
const removedSourceIds = [];
|
|
457
|
+
const warnings = [];
|
|
458
|
+
for (const source of lockFile.sources) {
|
|
459
|
+
if (await pathExists(source.checkoutPath)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
removedSourceIds.push(source.id);
|
|
463
|
+
warnings.push({
|
|
464
|
+
code: "SOURCE_CHECKOUT_MISSING",
|
|
465
|
+
message: `Removed ${source.displayName} because checkout is missing at ${source.checkoutPath}.`,
|
|
466
|
+
});
|
|
467
|
+
const deployments = lockFile.deployments.filter((deployment) => deployment.sourceId === source.id);
|
|
468
|
+
for (const deployment of deployments) {
|
|
469
|
+
if (!(await pathExists(deployment.targetPath))) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
await removePath(deployment.targetPath);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
return fail({
|
|
477
|
+
code: "SOURCE_CHECKOUT_PRUNE_FAILED",
|
|
478
|
+
message: `Unable to clean deployment ${deployment.targetPath}: ${String(error)}`,
|
|
479
|
+
}, warnings);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (removedSourceIds.length === 0) {
|
|
484
|
+
return ok({ removedSourceIds: [] });
|
|
485
|
+
}
|
|
486
|
+
manifest.sources = manifest.sources.filter((source) => !removedSourceIds.includes(source.id));
|
|
487
|
+
for (const sourceId of removedSourceIds) {
|
|
488
|
+
delete manifest.bindings[sourceId];
|
|
489
|
+
}
|
|
490
|
+
lockFile.sources = lockFile.sources.filter((source) => !removedSourceIds.includes(source.id));
|
|
491
|
+
lockFile.leafInventory = lockFile.leafInventory.filter((leaf) => !removedSourceIds.includes(leaf.sourceId));
|
|
492
|
+
lockFile.deployments = lockFile.deployments.filter((deployment) => !removedSourceIds.includes(deployment.sourceId));
|
|
493
|
+
await this.store.writeState(manifest, lockFile);
|
|
494
|
+
return ok({ removedSourceIds }, warnings);
|
|
495
|
+
}
|
|
297
496
|
async persistNormalizedBindings(manifest, lockFile) {
|
|
298
497
|
if (!this.normalizeBindings(manifest, lockFile)) {
|
|
299
498
|
return;
|
|
300
499
|
}
|
|
301
|
-
await this.store.
|
|
500
|
+
await this.store.writeState(manifest, lockFile);
|
|
501
|
+
}
|
|
502
|
+
async runSerializedMutation(task) {
|
|
503
|
+
const run = this.mutationQueue.then(() => this.store.withMutationLock(task), () => this.store.withMutationLock(task));
|
|
504
|
+
this.mutationQueue = run.then(() => undefined, () => undefined);
|
|
505
|
+
return run;
|
|
302
506
|
}
|
|
303
507
|
normalizeBindings(manifest, lockFile) {
|
|
304
508
|
let changed = false;
|
|
@@ -339,6 +543,14 @@ export class SkillFlowApp {
|
|
|
339
543
|
}
|
|
340
544
|
prepareManifestForDraft(manifest, lockFile, sourceId, draft) {
|
|
341
545
|
manifest.bindings[sourceId] = this.bindingFromDraft(draft);
|
|
546
|
+
const source = manifest.sources.find((item) => item.id === sourceId);
|
|
547
|
+
if (source) {
|
|
548
|
+
const sourceLeafCount = lockFile.leafInventory.filter((leaf) => leaf.sourceId === sourceId).length;
|
|
549
|
+
source.selectionMode =
|
|
550
|
+
draft.selectedLeafIds.length >= sourceLeafCount && sourceLeafCount > 0
|
|
551
|
+
? "all"
|
|
552
|
+
: "partial";
|
|
553
|
+
}
|
|
342
554
|
const conflictingLeafIds = this.findExactDuplicateLeafSelections(manifest, lockFile, sourceId, draft.enabledTargets);
|
|
343
555
|
const normalizedDraft = {
|
|
344
556
|
enabledTargets: [...draft.enabledTargets],
|
|
@@ -391,9 +603,13 @@ export class SkillFlowApp {
|
|
|
391
603
|
const sourceIds = manifest.sources
|
|
392
604
|
.map((source) => source.id)
|
|
393
605
|
.filter((sourceId) => sourceId === primarySourceId || this.hasActiveTargets(manifest, sourceId));
|
|
606
|
+
return this.planForSources(manifest, lockFile, sourceIds);
|
|
607
|
+
}
|
|
608
|
+
async planForSources(manifest, lockFile, sourceIds) {
|
|
609
|
+
const uniqueSourceIds = [...new Set(sourceIds)];
|
|
394
610
|
const actions = [];
|
|
395
611
|
const warnings = [];
|
|
396
|
-
for (const sourceId of
|
|
612
|
+
for (const sourceId of uniqueSourceIds) {
|
|
397
613
|
const plan = await this.planner.planForSource(sourceId, manifest, lockFile);
|
|
398
614
|
if (!plan.ok) {
|
|
399
615
|
return fail(plan.errors, [...warnings, ...plan.warnings]);
|
|
@@ -407,6 +623,148 @@ export class SkillFlowApp {
|
|
|
407
623
|
blocked: actions.filter((action) => action.kind === "blocked"),
|
|
408
624
|
}, warnings);
|
|
409
625
|
}
|
|
626
|
+
async planAndApplySources(manifest, lockFile, sourceIds) {
|
|
627
|
+
const planned = await this.planForSources(manifest, lockFile, sourceIds);
|
|
628
|
+
if (!planned.ok) {
|
|
629
|
+
return fail(planned.errors, planned.warnings);
|
|
630
|
+
}
|
|
631
|
+
const applyResult = await this.applier.applyPlan(lockFile, planned.data.actions);
|
|
632
|
+
if (!applyResult.ok) {
|
|
633
|
+
return fail(applyResult.errors, [...planned.warnings, ...applyResult.warnings]);
|
|
634
|
+
}
|
|
635
|
+
return ok({ actions: planned.data.actions }, [...planned.warnings, ...applyResult.warnings]);
|
|
636
|
+
}
|
|
637
|
+
async rebuildDeploymentState(manifest, lockFile, sourceIds) {
|
|
638
|
+
const requested = sourceIds?.length ? new Set(sourceIds) : undefined;
|
|
639
|
+
const previousDeployments = lockFile.deployments;
|
|
640
|
+
const previousCount = previousDeployments.length;
|
|
641
|
+
const previousByKey = new Map(previousDeployments.map((deployment) => [
|
|
642
|
+
this.getDeploymentKey(deployment.sourceId, deployment.leafId, deployment.target),
|
|
643
|
+
deployment,
|
|
644
|
+
]));
|
|
645
|
+
const nextDeployments = previousDeployments.filter((deployment) => requested ? !requested.has(deployment.sourceId) : false);
|
|
646
|
+
const adapters = createChannelAdapters();
|
|
647
|
+
const detectionCache = new Map();
|
|
648
|
+
const projectedNameCache = new Map();
|
|
649
|
+
for (const source of manifest.sources) {
|
|
650
|
+
if (requested && !requested.has(source.id)) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const binding = manifest.bindings[source.id] ?? { targets: {} };
|
|
654
|
+
for (const adapter of adapters) {
|
|
655
|
+
const targetBinding = binding.targets[adapter.target];
|
|
656
|
+
if (!targetBinding?.enabled) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
let detection = detectionCache.get(adapter.target);
|
|
660
|
+
if (!detection) {
|
|
661
|
+
detection = await adapter.detect();
|
|
662
|
+
detectionCache.set(adapter.target, detection);
|
|
663
|
+
}
|
|
664
|
+
for (const leafId of targetBinding.leafIds) {
|
|
665
|
+
const leaf = lockFile.leafInventory.find((candidate) => candidate.sourceId === source.id && candidate.id === leafId);
|
|
666
|
+
if (!leaf) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const existing = previousByKey.get(this.getDeploymentKey(source.id, leaf.id, adapter.target));
|
|
670
|
+
if (!detection.available) {
|
|
671
|
+
if (existing) {
|
|
672
|
+
nextDeployments.push({
|
|
673
|
+
...existing,
|
|
674
|
+
contentHash: leaf.contentHash,
|
|
675
|
+
status: "active",
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
let projectedLinkNames = projectedNameCache.get(adapter.target);
|
|
681
|
+
if (!projectedLinkNames) {
|
|
682
|
+
projectedLinkNames = this.buildProjectedLinkNameMap(manifest, lockFile, adapter.target);
|
|
683
|
+
projectedNameCache.set(adapter.target, projectedLinkNames);
|
|
684
|
+
}
|
|
685
|
+
const rebuilt = await this.findManagedDeploymentOnDisk(source, leaf, adapter.target, adapter.strategy, detection.rootPath, projectedLinkNames, existing);
|
|
686
|
+
if (!rebuilt) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
nextDeployments.push(rebuilt);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
lockFile.deployments = nextDeployments;
|
|
694
|
+
return Math.max(0, previousCount - nextDeployments.length);
|
|
695
|
+
}
|
|
696
|
+
buildProjectedLinkNameMap(manifest, lockFile, target) {
|
|
697
|
+
return resolveProjectedSkillNames(manifest.sources.flatMap((source) => {
|
|
698
|
+
const targetBinding = manifest.bindings[source.id]?.targets[target];
|
|
699
|
+
if (!targetBinding?.enabled) {
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
return targetBinding.leafIds
|
|
703
|
+
.map((leafId) => lockFile.leafInventory.find((leaf) => leaf.id === leafId))
|
|
704
|
+
.filter((leaf) => Boolean(leaf))
|
|
705
|
+
.map((leaf) => ({
|
|
706
|
+
leafId: leaf.id,
|
|
707
|
+
groupId: source.id,
|
|
708
|
+
groupName: source.displayName,
|
|
709
|
+
groupAuthor: parseGitHubRepo(source.locator)?.owner,
|
|
710
|
+
skillName: leaf.linkName,
|
|
711
|
+
}));
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
async findManagedDeploymentOnDisk(source, leaf, target, strategy, rootPath, projectedLinkNames, existing) {
|
|
715
|
+
const projectedLinkName = projectedLinkNames.get(leaf.id) ?? leaf.linkName;
|
|
716
|
+
const candidatePaths = buildProjectedSkillNameCandidates({
|
|
717
|
+
preferredName: projectedLinkName,
|
|
718
|
+
groupId: source.id,
|
|
719
|
+
groupName: source.displayName,
|
|
720
|
+
groupAuthor: parseGitHubRepo(source.locator)?.owner,
|
|
721
|
+
skillName: leaf.linkName,
|
|
722
|
+
}).map((name) => path.join(rootPath, name));
|
|
723
|
+
if (existing?.targetPath && !candidatePaths.includes(existing.targetPath)) {
|
|
724
|
+
candidatePaths.unshift(existing.targetPath);
|
|
725
|
+
}
|
|
726
|
+
for (const targetPath of candidatePaths) {
|
|
727
|
+
const matches = await this.matchesManagedProjection(strategy, targetPath, leaf);
|
|
728
|
+
if (!matches) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
sourceId: source.id,
|
|
733
|
+
leafId: leaf.id,
|
|
734
|
+
target,
|
|
735
|
+
targetPath,
|
|
736
|
+
strategy,
|
|
737
|
+
status: "active",
|
|
738
|
+
contentHash: leaf.contentHash,
|
|
739
|
+
appliedAt: existing?.appliedAt ?? new Date().toISOString(),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
async matchesManagedProjection(strategy, targetPath, leaf) {
|
|
745
|
+
try {
|
|
746
|
+
const stats = await fs.lstat(targetPath);
|
|
747
|
+
if (strategy === "symlink") {
|
|
748
|
+
if (!stats.isSymbolicLink()) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
const linked = await fs.readlink(targetPath);
|
|
752
|
+
const resolved = path.resolve(path.dirname(targetPath), linked);
|
|
753
|
+
return resolved === leaf.absolutePath;
|
|
754
|
+
}
|
|
755
|
+
if (!stats.isDirectory()) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
const onDiskHash = await hashDirectory(targetPath);
|
|
759
|
+
return onDiskHash === leaf.contentHash;
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
getDeploymentKey(sourceId, leafId, target) {
|
|
766
|
+
return `${sourceId}\n${leafId}\n${target}`;
|
|
767
|
+
}
|
|
410
768
|
hasActiveTargets(manifest, sourceId) {
|
|
411
769
|
const binding = manifest.bindings[sourceId];
|
|
412
770
|
if (!binding) {
|
|
@@ -428,7 +786,7 @@ export class SkillFlowApp {
|
|
|
428
786
|
const source = manifest.sources.find((item) => item.id === leaf.sourceId);
|
|
429
787
|
return {
|
|
430
788
|
id: `local:${leaf.id}`,
|
|
431
|
-
title: leaf
|
|
789
|
+
title: this.getCandidateTitle(leaf),
|
|
432
790
|
description: leaf.description,
|
|
433
791
|
source: "local",
|
|
434
792
|
sourceLabel: source
|
|
@@ -547,7 +905,7 @@ export class SkillFlowApp {
|
|
|
547
905
|
if (tokens.length === 0) {
|
|
548
906
|
return true;
|
|
549
907
|
}
|
|
550
|
-
const haystack = fields.join("\n")
|
|
908
|
+
const haystack = this.normalizeSearchQuery(fields.join("\n"));
|
|
551
909
|
return tokens.every((token) => haystack.includes(token));
|
|
552
910
|
}
|
|
553
911
|
compareCandidates(left, right, query) {
|
|
@@ -577,8 +935,8 @@ export class SkillFlowApp {
|
|
|
577
935
|
}
|
|
578
936
|
getQueryScore(candidate, query) {
|
|
579
937
|
const tokens = query.split(/\s+/).filter(Boolean);
|
|
580
|
-
const titleField = candidate.title
|
|
581
|
-
const pathTail = (candidate.relativePath ?? "").split("/").pop()
|
|
938
|
+
const titleField = this.normalizeSearchQuery(candidate.title);
|
|
939
|
+
const pathTail = this.normalizeSearchQuery((candidate.relativePath ?? "").split("/").pop() ?? "");
|
|
582
940
|
const fields = [
|
|
583
941
|
titleField,
|
|
584
942
|
pathTail,
|
|
@@ -604,12 +962,67 @@ export class SkillFlowApp {
|
|
|
604
962
|
}
|
|
605
963
|
return candidate.locator;
|
|
606
964
|
}
|
|
965
|
+
getCandidateTitle(leaf) {
|
|
966
|
+
const title = leaf.title.trim();
|
|
967
|
+
if (title.length === 0 || /^\{[^}]+\}$/.test(title)) {
|
|
968
|
+
return leaf.linkName || leaf.name;
|
|
969
|
+
}
|
|
970
|
+
return title;
|
|
971
|
+
}
|
|
972
|
+
normalizeSearchQuery(value) {
|
|
973
|
+
return value.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
|
|
974
|
+
}
|
|
607
975
|
formatSourceLabel(locator, displayName) {
|
|
976
|
+
if (locator.startsWith("clawhub:")) {
|
|
977
|
+
return `${displayName}@clawhub`;
|
|
978
|
+
}
|
|
979
|
+
if (path.isAbsolute(locator)) {
|
|
980
|
+
return `${displayName}@local`;
|
|
981
|
+
}
|
|
608
982
|
const repo = parseGitHubRepo(locator);
|
|
609
983
|
if (!repo) {
|
|
610
984
|
return displayName;
|
|
611
985
|
}
|
|
612
|
-
return `${displayName}
|
|
986
|
+
return `${displayName}@${repo.owner}`;
|
|
987
|
+
}
|
|
988
|
+
applySourceUpdateResults(manifest, lockFile, updates) {
|
|
989
|
+
for (const update of updates) {
|
|
990
|
+
if (!update.changed) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const source = manifest.sources.find((item) => item.id === update.sourceId);
|
|
994
|
+
const binding = manifest.bindings[update.sourceId];
|
|
995
|
+
if (!source || !binding) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
for (const diff of update.diffs) {
|
|
999
|
+
if (diff.kind !== "moved" || !diff.previousLeafId) {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
for (const targetBinding of Object.values(binding.targets)) {
|
|
1003
|
+
if (!targetBinding?.enabled || !targetBinding.leafIds.includes(diff.previousLeafId)) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
targetBinding.leafIds = targetBinding.leafIds.map((leafId) => leafId === diff.previousLeafId ? diff.leafId : leafId);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if ((update.selectionMode ?? source.selectionMode) !== "all") {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const addedLeafIds = update.diffs
|
|
1013
|
+
.filter((diff) => diff.kind === "added")
|
|
1014
|
+
.map((diff) => diff.leafId);
|
|
1015
|
+
if (addedLeafIds.length === 0) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
for (const targetBinding of Object.values(binding.targets)) {
|
|
1019
|
+
if (!targetBinding?.enabled) {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const merged = new Set([...targetBinding.leafIds, ...addedLeafIds]);
|
|
1023
|
+
targetBinding.leafIds = [...merged].filter((leafId) => lockFile.leafInventory.some((leaf) => leaf.id === leafId && leaf.sourceId === update.sourceId));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
613
1026
|
}
|
|
614
1027
|
}
|
|
615
1028
|
//# sourceMappingURL=skill-flow.js.map
|