skill-flow 1.0.4 → 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 +1 -1
- package/README.zh.md +1 -1
- package/dist/adapters/channel-adapters.js +11 -3
- package/dist/adapters/channel-adapters.js.map +1 -1
- package/dist/cli.js +64 -6
- package/dist/cli.js.map +1 -1
- package/dist/domain/types.d.ts +42 -0
- 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 +37 -16
- package/dist/services/skill-flow.js +446 -186
- package/dist/services/skill-flow.js.map +1 -1
- package/dist/services/source-service.d.ts +10 -10
- package/dist/services/source-service.js +306 -64
- package/dist/services/source-service.js.map +1 -1
- package/dist/services/workspace-bootstrap-service.js +2 -10
- package/dist/services/workspace-bootstrap-service.js.map +1 -1
- package/dist/state/store.d.ts +16 -0
- package/dist/state/store.js +93 -19
- 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 +311 -889
- 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 +144 -24
- package/dist/tui/config-app.js +1025 -339
- package/dist/tui/config-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/package.json +1 -1
|
@@ -2,14 +2,15 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createChannelAdapters } from "../adapters/channel-adapters.js";
|
|
4
4
|
import { StateStore } from "../state/store.js";
|
|
5
|
-
import { ensureDir, hashDirectory, pathExists, readJsonFile, removePath, writeJsonFile } from "../utils/fs.js";
|
|
5
|
+
import { ensureDir, hashDirectory, pathExists, readJsonFile, removePath, writeJsonFile, } from "../utils/fs.js";
|
|
6
6
|
import { getBuiltinGitSources } from "../utils/builtin-git-sources.js";
|
|
7
7
|
import { fetchGitHubSkillPaths } from "../utils/github-catalog.js";
|
|
8
|
-
import { parseGitHubRepo } from "../utils/naming.js";
|
|
8
|
+
import { buildProjectedSkillNameCandidates, parseGitHubRepo, resolveProjectedSkillNames, } from "../utils/naming.js";
|
|
9
9
|
import { fail, ok } from "../utils/result.js";
|
|
10
10
|
import { searchClawHubSkills } from "../utils/clawhub.js";
|
|
11
11
|
import { deriveDisplayName, deriveSourceId } from "../utils/source-id.js";
|
|
12
12
|
import { DeploymentApplier } from "./deployment-applier.js";
|
|
13
|
+
import { ConfigCoordinator } from "./config-coordinator.js";
|
|
13
14
|
import { DeploymentPlanner } from "./deployment-planner.js";
|
|
14
15
|
import { DoctorService } from "./doctor-service.js";
|
|
15
16
|
import { InventoryService } from "./inventory-service.js";
|
|
@@ -17,23 +18,44 @@ import { SourceService } from "./source-service.js";
|
|
|
17
18
|
import { WorkflowService } from "./workflow-service.js";
|
|
18
19
|
import { WorkspaceBootstrapService, } from "./workspace-bootstrap-service.js";
|
|
19
20
|
export class SkillFlowApp {
|
|
20
|
-
store
|
|
21
|
-
inventoryService
|
|
22
|
-
sourceService
|
|
23
|
-
planner
|
|
24
|
-
applier
|
|
25
|
-
doctorService
|
|
26
|
-
workflowService
|
|
27
|
-
workspaceBootstrapService
|
|
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
|
+
}
|
|
28
49
|
async addSource(locator, options) {
|
|
50
|
+
return this.runSerializedMutation(() => this.addSourceImpl(locator, options));
|
|
51
|
+
}
|
|
52
|
+
async addSourceImpl(locator, options) {
|
|
29
53
|
const addOptions = options ?? {};
|
|
30
54
|
const result = await this.sourceService.addSource(locator, addOptions);
|
|
31
55
|
if (!result.ok) {
|
|
32
56
|
return result;
|
|
33
57
|
}
|
|
34
|
-
await this.store.
|
|
35
|
-
const manifest = await this.store.readManifest();
|
|
36
|
-
const lockFile = await this.store.readLock();
|
|
58
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
37
59
|
const source = manifest.sources.find((item) => item.id === result.data.manifest.id);
|
|
38
60
|
if (!source) {
|
|
39
61
|
return result;
|
|
@@ -47,7 +69,7 @@ export class SkillFlowApp {
|
|
|
47
69
|
enabledTargets,
|
|
48
70
|
selectedLeafIds: this.selectLeafIdsForRequestedPath(lockFile.leafInventory.filter((leaf) => leaf.sourceId === source.id), source.requestedPath),
|
|
49
71
|
});
|
|
50
|
-
await this.store.
|
|
72
|
+
await this.store.writeState(manifest, lockFile);
|
|
51
73
|
if (addOptions.project === false) {
|
|
52
74
|
return result;
|
|
53
75
|
}
|
|
@@ -56,17 +78,15 @@ export class SkillFlowApp {
|
|
|
56
78
|
return fail(plan.errors, [...result.warnings, ...plan.warnings]);
|
|
57
79
|
}
|
|
58
80
|
const applied = await this.applier.applyPlan(lockFile, plan.data.actions);
|
|
59
|
-
await this.store.
|
|
81
|
+
await this.store.writeState(manifest, lockFile);
|
|
60
82
|
if (!applied.ok) {
|
|
61
83
|
return fail(applied.errors, [...result.warnings, ...plan.warnings, ...applied.warnings]);
|
|
62
84
|
}
|
|
63
85
|
return ok(result.data, [...result.warnings, ...plan.warnings, ...applied.warnings]);
|
|
64
86
|
}
|
|
65
87
|
async findSkills(query) {
|
|
66
|
-
await this.store.
|
|
67
|
-
const
|
|
68
|
-
const lockFile = await this.store.readLock();
|
|
69
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
88
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
89
|
+
const normalizedQuery = this.normalizeSearchQuery(query);
|
|
70
90
|
const warnings = [];
|
|
71
91
|
const localKeys = new Set();
|
|
72
92
|
const candidates = [];
|
|
@@ -111,7 +131,7 @@ export class SkillFlowApp {
|
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
try {
|
|
114
|
-
const results = await searchClawHubSkills(
|
|
134
|
+
const results = await searchClawHubSkills(normalizedQuery, 8);
|
|
115
135
|
remoteSearchSucceeded = true;
|
|
116
136
|
for (const result of results) {
|
|
117
137
|
candidates.push({
|
|
@@ -147,124 +167,63 @@ export class SkillFlowApp {
|
|
|
147
167
|
return ok({ candidates }, warnings);
|
|
148
168
|
}
|
|
149
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
|
+
}
|
|
150
177
|
const reconciled = await this.sourceService.reconcileInventory(undefined, {
|
|
151
178
|
force: true,
|
|
152
179
|
});
|
|
153
180
|
if (!reconciled.ok) {
|
|
154
181
|
return fail(reconciled.errors, reconciled.warnings);
|
|
155
182
|
}
|
|
156
|
-
await this.store.
|
|
157
|
-
const manifest = await this.store.readManifest();
|
|
158
|
-
const lockFile = await this.store.readLock();
|
|
183
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
159
184
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
160
185
|
return ok({
|
|
161
186
|
summaries: this.workflowService.getSummaries(manifest, lockFile),
|
|
162
|
-
});
|
|
187
|
+
}, pruned.warnings);
|
|
163
188
|
}
|
|
164
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
|
+
}
|
|
165
197
|
const reconciled = await this.sourceService.reconcileInventory(undefined, {
|
|
166
198
|
force: true,
|
|
167
199
|
});
|
|
168
200
|
if (!reconciled.ok) {
|
|
169
201
|
return fail(reconciled.errors, reconciled.warnings);
|
|
170
202
|
}
|
|
171
|
-
await this.store.
|
|
172
|
-
const manifest = await this.store.readManifest();
|
|
173
|
-
const lockFile = await this.store.readLock();
|
|
203
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
174
204
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
175
205
|
return ok({
|
|
176
206
|
manifest,
|
|
177
207
|
lockFile,
|
|
178
208
|
summaries: this.workflowService.getSummaries(manifest, lockFile),
|
|
179
|
-
});
|
|
209
|
+
}, pruned.warnings);
|
|
180
210
|
}
|
|
181
211
|
async bootstrapWorkspaceState(onEvent) {
|
|
182
|
-
onEvent
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await this.store.init();
|
|
189
|
-
let manifest = await this.store.readManifest();
|
|
190
|
-
let lockFile = await this.store.readLock();
|
|
191
|
-
const detected = await this.workspaceBootstrapService.detectUnmanagedExternalSkills(manifest, lockFile, onEvent);
|
|
192
|
-
const importedSourceIds = [];
|
|
193
|
-
if (detected.length > 0) {
|
|
194
|
-
onEvent?.({
|
|
195
|
-
phase: "import-unmanaged-skills",
|
|
196
|
-
level: "info",
|
|
197
|
-
message: `Importing ${detected.length} unmanaged skill${detected.length === 1 ? "" : "s"} into skill-flow...`,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
for (const item of detected) {
|
|
201
|
-
const imported = await this.addSource(item.path, {
|
|
202
|
-
enabledTargets: item.importedFromTargets,
|
|
203
|
-
selectionMode: "all",
|
|
204
|
-
project: false,
|
|
205
|
-
sourceIdOverride: item.sourceId,
|
|
206
|
-
displayNameOverride: item.displayName,
|
|
207
|
-
importedFromTargets: item.importedFromTargets,
|
|
208
|
-
importMode: "bootstrap-detected",
|
|
209
|
-
...(item.originLocator ? { originLocator: item.originLocator } : {}),
|
|
210
|
-
...(item.originRequestedPath ? { originRequestedPath: item.originRequestedPath } : {}),
|
|
211
|
-
...(item.originBranch ? { originBranch: item.originBranch } : {}),
|
|
212
|
-
});
|
|
213
|
-
if (!imported.ok) {
|
|
214
|
-
return fail(imported.errors, imported.warnings);
|
|
215
|
-
}
|
|
216
|
-
importedSourceIds.push(imported.data.manifest.id);
|
|
217
|
-
onEvent?.({
|
|
218
|
-
phase: "import-unmanaged-skills",
|
|
219
|
-
level: "success",
|
|
220
|
-
message: `Imported ${imported.data.manifest.displayName}.`,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
onEvent?.({
|
|
224
|
-
phase: "refresh-sources",
|
|
225
|
-
level: "info",
|
|
226
|
-
message: "Refreshing managed inventory...",
|
|
227
|
-
});
|
|
228
|
-
const reconciled = await this.sourceService.reconcileInventory(undefined, { force: true });
|
|
229
|
-
if (!reconciled.ok) {
|
|
230
|
-
return fail(reconciled.errors, reconciled.warnings);
|
|
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);
|
|
231
218
|
}
|
|
232
|
-
manifest = await this.store.readManifest();
|
|
233
|
-
lockFile = await this.store.readLock();
|
|
234
|
-
onEvent?.({
|
|
235
|
-
phase: "normalize-bindings",
|
|
236
|
-
level: "info",
|
|
237
|
-
message: "Normalizing config state...",
|
|
238
|
-
});
|
|
239
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
240
|
-
onEvent?.({
|
|
241
|
-
phase: "audit-projections",
|
|
242
|
-
level: "info",
|
|
243
|
-
message: "Auditing current projections...",
|
|
244
|
-
});
|
|
245
|
-
const audit = await this.doctorService.run(manifest, lockFile);
|
|
246
|
-
if (!audit.ok) {
|
|
247
|
-
return fail(audit.errors, audit.warnings);
|
|
248
|
-
}
|
|
249
|
-
onEvent?.({
|
|
250
|
-
phase: "build-summaries",
|
|
251
|
-
level: "info",
|
|
252
|
-
message: "Building config summaries...",
|
|
253
|
-
});
|
|
254
|
-
const summaries = this.workflowService.getSummaries(manifest, lockFile, audit.data);
|
|
255
|
-
onEvent?.({
|
|
256
|
-
phase: "done",
|
|
257
|
-
level: "success",
|
|
258
|
-
message: "Config bootstrap complete.",
|
|
259
|
-
});
|
|
260
219
|
return ok({
|
|
261
|
-
availableTargets,
|
|
262
|
-
manifest,
|
|
263
|
-
lockFile,
|
|
264
|
-
summaries,
|
|
265
|
-
initialDrafts:
|
|
266
|
-
audit:
|
|
267
|
-
importedSourceIds,
|
|
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: [],
|
|
268
227
|
});
|
|
269
228
|
}
|
|
270
229
|
async getAvailableTargets() {
|
|
@@ -282,9 +241,7 @@ export class SkillFlowApp {
|
|
|
282
241
|
// config TUI state flow:
|
|
283
242
|
// draft -> previewDraft() -> plan only
|
|
284
243
|
// draft -> applyDraft() -> plan + filesystem + manifest/lock writes
|
|
285
|
-
await this.store.
|
|
286
|
-
const manifest = await this.store.readManifest();
|
|
287
|
-
const lockFile = await this.store.readLock();
|
|
244
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
288
245
|
this.normalizeBindings(manifest, lockFile);
|
|
289
246
|
const prepared = this.prepareManifestForDraft(manifest, lockFile, sourceId, draft);
|
|
290
247
|
const plan = await this.planForAffectedSources(prepared.manifest, lockFile, sourceId);
|
|
@@ -294,15 +251,10 @@ export class SkillFlowApp {
|
|
|
294
251
|
return ok({ plan: plan.data, manifest: prepared.manifest, lockFile }, [...prepared.warnings, ...plan.warnings]);
|
|
295
252
|
}
|
|
296
253
|
async applyDraft(sourceId, draft) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return fail(reconciled.errors, reconciled.warnings);
|
|
302
|
-
}
|
|
303
|
-
await this.store.init();
|
|
304
|
-
const manifest = await this.store.readManifest();
|
|
305
|
-
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();
|
|
306
258
|
this.normalizeBindings(manifest, lockFile);
|
|
307
259
|
const prepared = this.prepareManifestForDraft(manifest, lockFile, sourceId, draft);
|
|
308
260
|
const plan = await this.planForAffectedSources(prepared.manifest, lockFile, sourceId);
|
|
@@ -310,56 +262,143 @@ export class SkillFlowApp {
|
|
|
310
262
|
return fail(plan.errors, [...prepared.warnings, ...plan.warnings]);
|
|
311
263
|
}
|
|
312
264
|
const applyResult = await this.applier.applyPlan(lockFile, plan.data.actions);
|
|
313
|
-
await this.store.
|
|
314
|
-
await this.store.writeLock(lockFile);
|
|
265
|
+
await this.store.writeState(prepared.manifest, lockFile);
|
|
315
266
|
if (!applyResult.ok) {
|
|
316
267
|
return fail(applyResult.errors, [...prepared.warnings, ...plan.warnings, ...applyResult.warnings]);
|
|
317
268
|
}
|
|
318
269
|
return ok({ actions: plan.data.actions, draft: prepared.draft }, [...prepared.warnings, ...plan.warnings, ...applyResult.warnings]);
|
|
319
270
|
}
|
|
320
271
|
async updateSources(sourceIds) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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);
|
|
326
278
|
}
|
|
327
|
-
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);
|
|
328
284
|
if (!updated.ok) {
|
|
329
285
|
return updated;
|
|
330
286
|
}
|
|
331
|
-
const manifest = await this.store.
|
|
332
|
-
|
|
333
|
-
this.applySelectionModeForUpdatedSources(manifest, lockFile, updated.data.updated);
|
|
287
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
288
|
+
this.applySourceUpdateResults(manifest, lockFile, updated.data.updated);
|
|
334
289
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
335
|
-
const
|
|
290
|
+
const planSourceIds = manifest.sources
|
|
336
291
|
.map((source) => source.id)
|
|
337
|
-
.filter((id) =>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
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]);
|
|
344
298
|
}
|
|
345
|
-
await this.store.
|
|
346
|
-
return updated;
|
|
299
|
+
await this.store.writeState(manifest, lockFile);
|
|
300
|
+
return ok(updated.data, [...pruned.warnings, ...updated.warnings, ...planned.warnings]);
|
|
347
301
|
}
|
|
348
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
|
+
}
|
|
349
310
|
const reconciled = await this.sourceService.reconcileInventory();
|
|
350
311
|
if (!reconciled.ok) {
|
|
351
312
|
return fail(reconciled.errors, reconciled.warnings);
|
|
352
313
|
}
|
|
353
|
-
await this.store.
|
|
354
|
-
|
|
355
|
-
const
|
|
314
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
315
|
+
await this.persistNormalizedBindings(manifest, lockFile);
|
|
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();
|
|
356
389
|
await this.persistNormalizedBindings(manifest, lockFile);
|
|
357
|
-
|
|
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]);
|
|
358
396
|
}
|
|
359
397
|
async uninstall(sourceIds) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
398
|
+
return this.runSerializedMutation(() => this.uninstallImpl(sourceIds));
|
|
399
|
+
}
|
|
400
|
+
async uninstallImpl(sourceIds) {
|
|
401
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
363
402
|
const warnings = [];
|
|
364
403
|
const removedRefs = sourceIds
|
|
365
404
|
.map((sourceId) => manifest.sources.find((source) => source.id === sourceId))
|
|
@@ -370,26 +409,33 @@ export class SkillFlowApp {
|
|
|
370
409
|
if (!(await pathExists(deployment.targetPath))) {
|
|
371
410
|
continue;
|
|
372
411
|
}
|
|
373
|
-
|
|
374
|
-
const stats = await fs.lstat(deployment.targetPath);
|
|
375
|
-
if (stats.isSymbolicLink()) {
|
|
376
|
-
await removePath(deployment.targetPath);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
warnings.push(`Skipped ${deployment.targetPath} because it no longer looks like a managed symlink.`);
|
|
380
|
-
}
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const currentHash = await hashDirectory(deployment.targetPath);
|
|
384
|
-
if (currentHash === deployment.contentHash) {
|
|
412
|
+
try {
|
|
385
413
|
await removePath(deployment.targetPath);
|
|
386
414
|
}
|
|
387
|
-
|
|
388
|
-
warnings.push(`
|
|
415
|
+
catch (error) {
|
|
416
|
+
warnings.push(`Unable to remove ${deployment.targetPath}: ${String(error)}`);
|
|
389
417
|
}
|
|
390
418
|
}
|
|
391
419
|
}
|
|
392
|
-
|
|
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
|
+
}
|
|
393
439
|
if (!removed.ok) {
|
|
394
440
|
return fail(removed.errors, removed.warnings);
|
|
395
441
|
}
|
|
@@ -405,11 +451,58 @@ export class SkillFlowApp {
|
|
|
405
451
|
}
|
|
406
452
|
return { targets };
|
|
407
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
|
+
}
|
|
408
496
|
async persistNormalizedBindings(manifest, lockFile) {
|
|
409
497
|
if (!this.normalizeBindings(manifest, lockFile)) {
|
|
410
498
|
return;
|
|
411
499
|
}
|
|
412
|
-
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;
|
|
413
506
|
}
|
|
414
507
|
normalizeBindings(manifest, lockFile) {
|
|
415
508
|
let changed = false;
|
|
@@ -510,9 +603,13 @@ export class SkillFlowApp {
|
|
|
510
603
|
const sourceIds = manifest.sources
|
|
511
604
|
.map((source) => source.id)
|
|
512
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)];
|
|
513
610
|
const actions = [];
|
|
514
611
|
const warnings = [];
|
|
515
|
-
for (const sourceId of
|
|
612
|
+
for (const sourceId of uniqueSourceIds) {
|
|
516
613
|
const plan = await this.planner.planForSource(sourceId, manifest, lockFile);
|
|
517
614
|
if (!plan.ok) {
|
|
518
615
|
return fail(plan.errors, [...warnings, ...plan.warnings]);
|
|
@@ -526,6 +623,148 @@ export class SkillFlowApp {
|
|
|
526
623
|
blocked: actions.filter((action) => action.kind === "blocked"),
|
|
527
624
|
}, warnings);
|
|
528
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
|
+
}
|
|
529
768
|
hasActiveTargets(manifest, sourceId) {
|
|
530
769
|
const binding = manifest.bindings[sourceId];
|
|
531
770
|
if (!binding) {
|
|
@@ -547,7 +786,7 @@ export class SkillFlowApp {
|
|
|
547
786
|
const source = manifest.sources.find((item) => item.id === leaf.sourceId);
|
|
548
787
|
return {
|
|
549
788
|
id: `local:${leaf.id}`,
|
|
550
|
-
title: leaf
|
|
789
|
+
title: this.getCandidateTitle(leaf),
|
|
551
790
|
description: leaf.description,
|
|
552
791
|
source: "local",
|
|
553
792
|
sourceLabel: source
|
|
@@ -666,7 +905,7 @@ export class SkillFlowApp {
|
|
|
666
905
|
if (tokens.length === 0) {
|
|
667
906
|
return true;
|
|
668
907
|
}
|
|
669
|
-
const haystack = fields.join("\n")
|
|
908
|
+
const haystack = this.normalizeSearchQuery(fields.join("\n"));
|
|
670
909
|
return tokens.every((token) => haystack.includes(token));
|
|
671
910
|
}
|
|
672
911
|
compareCandidates(left, right, query) {
|
|
@@ -696,8 +935,8 @@ export class SkillFlowApp {
|
|
|
696
935
|
}
|
|
697
936
|
getQueryScore(candidate, query) {
|
|
698
937
|
const tokens = query.split(/\s+/).filter(Boolean);
|
|
699
|
-
const titleField = candidate.title
|
|
700
|
-
const pathTail = (candidate.relativePath ?? "").split("/").pop()
|
|
938
|
+
const titleField = this.normalizeSearchQuery(candidate.title);
|
|
939
|
+
const pathTail = this.normalizeSearchQuery((candidate.relativePath ?? "").split("/").pop() ?? "");
|
|
701
940
|
const fields = [
|
|
702
941
|
titleField,
|
|
703
942
|
pathTail,
|
|
@@ -723,6 +962,16 @@ export class SkillFlowApp {
|
|
|
723
962
|
}
|
|
724
963
|
return candidate.locator;
|
|
725
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
|
+
}
|
|
726
975
|
formatSourceLabel(locator, displayName) {
|
|
727
976
|
if (locator.startsWith("clawhub:")) {
|
|
728
977
|
return `${displayName}@clawhub`;
|
|
@@ -736,30 +985,41 @@ export class SkillFlowApp {
|
|
|
736
985
|
}
|
|
737
986
|
return `${displayName}@${repo.owner}`;
|
|
738
987
|
}
|
|
739
|
-
|
|
740
|
-
return Object.fromEntries(summaries.map((summary) => {
|
|
741
|
-
const enabledTargets = Object.entries(summary.bindings.targets)
|
|
742
|
-
.filter(([, value]) => value?.enabled)
|
|
743
|
-
.map(([target]) => target);
|
|
744
|
-
const selectedLeafIds = [...new Set(enabledTargets.flatMap((target) => summary.bindings.targets[target]?.leafIds ?? []))];
|
|
745
|
-
return [summary.source.id, { enabledTargets, selectedLeafIds }];
|
|
746
|
-
}));
|
|
747
|
-
}
|
|
748
|
-
applySelectionModeForUpdatedSources(manifest, lockFile, updates) {
|
|
988
|
+
applySourceUpdateResults(manifest, lockFile, updates) {
|
|
749
989
|
for (const update of updates) {
|
|
750
|
-
if (!update.changed
|
|
990
|
+
if (!update.changed) {
|
|
751
991
|
continue;
|
|
752
992
|
}
|
|
753
993
|
const source = manifest.sources.find((item) => item.id === update.sourceId);
|
|
754
994
|
const binding = manifest.bindings[update.sourceId];
|
|
755
|
-
if (!source || !binding
|
|
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) {
|
|
756
1016
|
continue;
|
|
757
1017
|
}
|
|
758
1018
|
for (const targetBinding of Object.values(binding.targets)) {
|
|
759
1019
|
if (!targetBinding?.enabled) {
|
|
760
1020
|
continue;
|
|
761
1021
|
}
|
|
762
|
-
const merged = new Set([...targetBinding.leafIds, ...
|
|
1022
|
+
const merged = new Set([...targetBinding.leafIds, ...addedLeafIds]);
|
|
763
1023
|
targetBinding.leafIds = [...merged].filter((leafId) => lockFile.leafInventory.some((leaf) => leaf.id === leafId && leaf.sourceId === update.sourceId));
|
|
764
1024
|
}
|
|
765
1025
|
}
|