skill-flow 1.0.8 → 1.3.3
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 +209 -131
- package/README.zh.md +170 -131
- package/dist/bridge-command.d.ts +9 -0
- package/dist/bridge-command.js +422 -0
- package/dist/bridge-command.js.map +1 -0
- package/dist/cli.js +68 -8
- package/dist/cli.js.map +1 -1
- package/package.json +11 -2
- package/dist/adapters/channel-adapters.d.ts +0 -8
- package/dist/adapters/channel-adapters.js +0 -64
- package/dist/adapters/channel-adapters.js.map +0 -1
- package/dist/domain/types.d.ts +0 -234
- package/dist/domain/types.js +0 -2
- package/dist/domain/types.js.map +0 -1
- package/dist/services/config-coordinator.d.ts +0 -38
- package/dist/services/config-coordinator.js +0 -83
- package/dist/services/config-coordinator.js.map +0 -1
- package/dist/services/deployment-applier.d.ts +0 -10
- package/dist/services/deployment-applier.js +0 -84
- package/dist/services/deployment-applier.js.map +0 -1
- package/dist/services/deployment-planner.d.ts +0 -16
- package/dist/services/deployment-planner.js +0 -366
- package/dist/services/deployment-planner.js.map +0 -1
- package/dist/services/doctor-service.d.ts +0 -7
- package/dist/services/doctor-service.js +0 -204
- package/dist/services/doctor-service.js.map +0 -1
- package/dist/services/inventory-service.d.ts +0 -17
- package/dist/services/inventory-service.js +0 -216
- package/dist/services/inventory-service.js.map +0 -1
- package/dist/services/skill-flow.d.ts +0 -136
- package/dist/services/skill-flow.js +0 -1210
- package/dist/services/skill-flow.js.map +0 -1
- package/dist/services/source-service.d.ts +0 -57
- package/dist/services/source-service.js +0 -809
- package/dist/services/source-service.js.map +0 -1
- package/dist/services/workflow-service.d.ts +0 -5
- package/dist/services/workflow-service.js +0 -45
- package/dist/services/workflow-service.js.map +0 -1
- package/dist/services/workspace-bootstrap-service.d.ts +0 -25
- package/dist/services/workspace-bootstrap-service.js +0 -140
- package/dist/services/workspace-bootstrap-service.js.map +0 -1
- package/dist/state/store.d.ts +0 -35
- package/dist/state/store.js +0 -151
- package/dist/state/store.js.map +0 -1
- package/dist/tests/add-flow-model.test.d.ts +0 -1
- package/dist/tests/add-flow-model.test.js +0 -108
- package/dist/tests/add-flow-model.test.js.map +0 -1
- package/dist/tests/add-flow-ui.test.d.ts +0 -1
- package/dist/tests/add-flow-ui.test.js +0 -16
- package/dist/tests/add-flow-ui.test.js.map +0 -1
- package/dist/tests/add-prepare-flow.test.d.ts +0 -1
- package/dist/tests/add-prepare-flow.test.js +0 -166
- package/dist/tests/add-prepare-flow.test.js.map +0 -1
- package/dist/tests/add-selection-and-find-command.test.d.ts +0 -1
- package/dist/tests/add-selection-and-find-command.test.js +0 -89
- package/dist/tests/add-selection-and-find-command.test.js.map +0 -1
- package/dist/tests/clawhub.test.d.ts +0 -1
- package/dist/tests/clawhub.test.js +0 -63
- package/dist/tests/clawhub.test.js.map +0 -1
- package/dist/tests/cli-utils.test.d.ts +0 -1
- package/dist/tests/cli-utils.test.js +0 -24
- package/dist/tests/cli-utils.test.js.map +0 -1
- package/dist/tests/config-coordinator.test.d.ts +0 -1
- package/dist/tests/config-coordinator.test.js +0 -219
- package/dist/tests/config-coordinator.test.js.map +0 -1
- package/dist/tests/config-integration.test.d.ts +0 -1
- package/dist/tests/config-integration.test.js +0 -276
- package/dist/tests/config-integration.test.js.map +0 -1
- package/dist/tests/config-ui-utils.test.d.ts +0 -1
- package/dist/tests/config-ui-utils.test.js +0 -523
- package/dist/tests/config-ui-utils.test.js.map +0 -1
- package/dist/tests/find-and-naming-utils.test.d.ts +0 -1
- package/dist/tests/find-and-naming-utils.test.js +0 -127
- package/dist/tests/find-and-naming-utils.test.js.map +0 -1
- package/dist/tests/inventory-service-precedence.test.d.ts +0 -1
- package/dist/tests/inventory-service-precedence.test.js +0 -42
- package/dist/tests/inventory-service-precedence.test.js.map +0 -1
- package/dist/tests/skill-flow.test.d.ts +0 -1
- package/dist/tests/skill-flow.test.js +0 -991
- package/dist/tests/skill-flow.test.js.map +0 -1
- package/dist/tests/source-lifecycle.test.d.ts +0 -1
- package/dist/tests/source-lifecycle.test.js +0 -644
- package/dist/tests/source-lifecycle.test.js.map +0 -1
- package/dist/tests/source-parsing-compatibility.test.d.ts +0 -1
- package/dist/tests/source-parsing-compatibility.test.js +0 -72
- package/dist/tests/source-parsing-compatibility.test.js.map +0 -1
- package/dist/tests/target-definitions.test.d.ts +0 -1
- package/dist/tests/target-definitions.test.js +0 -51
- package/dist/tests/target-definitions.test.js.map +0 -1
- package/dist/tests/test-helpers.d.ts +0 -18
- package/dist/tests/test-helpers.js +0 -123
- package/dist/tests/test-helpers.js.map +0 -1
- package/dist/tui/add-flow-model.d.ts +0 -62
- package/dist/tui/add-flow-model.js +0 -206
- package/dist/tui/add-flow-model.js.map +0 -1
- package/dist/tui/add-flow.d.ts +0 -25
- package/dist/tui/add-flow.js +0 -534
- package/dist/tui/add-flow.js.map +0 -1
- package/dist/tui/config-app.d.ts +0 -178
- package/dist/tui/config-app.js +0 -1551
- package/dist/tui/config-app.js.map +0 -1
- package/dist/tui/find-app.d.ts +0 -9
- package/dist/tui/find-app.js +0 -150
- package/dist/tui/find-app.js.map +0 -1
- package/dist/tui/selection-state.d.ts +0 -8
- package/dist/tui/selection-state.js +0 -32
- package/dist/tui/selection-state.js.map +0 -1
- package/dist/utils/builtin-git-sources.d.ts +0 -5
- package/dist/utils/builtin-git-sources.js +0 -23
- package/dist/utils/builtin-git-sources.js.map +0 -1
- package/dist/utils/clawhub.d.ts +0 -41
- package/dist/utils/clawhub.js +0 -94
- package/dist/utils/clawhub.js.map +0 -1
- package/dist/utils/cli.d.ts +0 -2
- package/dist/utils/cli.js +0 -19
- package/dist/utils/cli.js.map +0 -1
- package/dist/utils/constants.d.ts +0 -23
- package/dist/utils/constants.js +0 -195
- package/dist/utils/constants.js.map +0 -1
- package/dist/utils/find-command.d.ts +0 -2
- package/dist/utils/find-command.js +0 -29
- package/dist/utils/find-command.js.map +0 -1
- package/dist/utils/format.d.ts +0 -7
- package/dist/utils/format.js +0 -68
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -16
- package/dist/utils/fs.js +0 -144
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/git.d.ts +0 -3
- package/dist/utils/git.js +0 -12
- package/dist/utils/git.js.map +0 -1
- package/dist/utils/github-catalog.d.ts +0 -1
- package/dist/utils/github-catalog.js +0 -25
- package/dist/utils/github-catalog.js.map +0 -1
- package/dist/utils/naming.d.ts +0 -29
- package/dist/utils/naming.js +0 -115
- package/dist/utils/naming.js.map +0 -1
- package/dist/utils/result.d.ts +0 -4
- package/dist/utils/result.js +0 -15
- package/dist/utils/result.js.map +0 -1
- package/dist/utils/source-id.d.ts +0 -2
- package/dist/utils/source-id.js +0 -49
- package/dist/utils/source-id.js.map +0 -1
|
@@ -1,1210 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createChannelAdapters } from "../adapters/channel-adapters.js";
|
|
4
|
-
import { StateStore } from "../state/store.js";
|
|
5
|
-
import { ensureDir, hashDirectory, isPathInside, pathExists, readJsonFile, removePath, writeJsonFile, } from "../utils/fs.js";
|
|
6
|
-
import { getBuiltinGitSources } from "../utils/builtin-git-sources.js";
|
|
7
|
-
import { fetchGitHubSkillPaths } from "../utils/github-catalog.js";
|
|
8
|
-
import { buildProjectedSkillNameCandidates, parseGitHubRepo, resolveProjectedSkillNames, } from "../utils/naming.js";
|
|
9
|
-
import { fail, ok } from "../utils/result.js";
|
|
10
|
-
import { searchClawHubSkills } from "../utils/clawhub.js";
|
|
11
|
-
import { deriveDisplayName, deriveSourceId } from "../utils/source-id.js";
|
|
12
|
-
import { DeploymentApplier } from "./deployment-applier.js";
|
|
13
|
-
import { ConfigCoordinator } from "./config-coordinator.js";
|
|
14
|
-
import { DeploymentPlanner } from "./deployment-planner.js";
|
|
15
|
-
import { DoctorService } from "./doctor-service.js";
|
|
16
|
-
import { InventoryService } from "./inventory-service.js";
|
|
17
|
-
import { SourceService } from "./source-service.js";
|
|
18
|
-
import { WorkflowService } from "./workflow-service.js";
|
|
19
|
-
import { WorkspaceBootstrapService, } from "./workspace-bootstrap-service.js";
|
|
20
|
-
export class SkillFlowApp {
|
|
21
|
-
store;
|
|
22
|
-
adapters;
|
|
23
|
-
inventoryService;
|
|
24
|
-
sourceService;
|
|
25
|
-
planner;
|
|
26
|
-
applier;
|
|
27
|
-
doctorService;
|
|
28
|
-
workflowService;
|
|
29
|
-
workspaceBootstrapService;
|
|
30
|
-
configCoordinator;
|
|
31
|
-
mutationQueue = Promise.resolve();
|
|
32
|
-
constructor() {
|
|
33
|
-
const adapters = createChannelAdapters();
|
|
34
|
-
this.store = new StateStore();
|
|
35
|
-
this.adapters = adapters;
|
|
36
|
-
this.inventoryService = new InventoryService();
|
|
37
|
-
this.sourceService = new SourceService(this.store, this.inventoryService);
|
|
38
|
-
this.planner = new DeploymentPlanner(adapters);
|
|
39
|
-
this.applier = new DeploymentApplier(adapters);
|
|
40
|
-
this.doctorService = new DoctorService();
|
|
41
|
-
this.workflowService = new WorkflowService();
|
|
42
|
-
this.workspaceBootstrapService = new WorkspaceBootstrapService(this.store);
|
|
43
|
-
this.configCoordinator = new ConfigCoordinator({
|
|
44
|
-
store: this.store,
|
|
45
|
-
doctorService: this.doctorService,
|
|
46
|
-
workflowService: this.workflowService,
|
|
47
|
-
getAvailableTargets: () => this.getAvailableTargets(),
|
|
48
|
-
pruneMissingCheckouts: () => this.pruneMissingCheckoutsImpl(),
|
|
49
|
-
getConfigData: () => this.getConfigDataImpl(),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
async addSource(locator, options) {
|
|
53
|
-
return this.runSerializedMutation(() => this.addSourceImpl(locator, options));
|
|
54
|
-
}
|
|
55
|
-
async prepareAddSource(locator, options) {
|
|
56
|
-
return this.runSerializedMutation(() => this.prepareAddSourceImpl(locator, options));
|
|
57
|
-
}
|
|
58
|
-
async addSourceImpl(locator, options) {
|
|
59
|
-
const prepared = await this.prepareAddSourceImpl(locator, options);
|
|
60
|
-
if (!prepared.ok) {
|
|
61
|
-
return prepared;
|
|
62
|
-
}
|
|
63
|
-
const addOptions = options ?? {};
|
|
64
|
-
if (addOptions.project === false) {
|
|
65
|
-
return prepared;
|
|
66
|
-
}
|
|
67
|
-
const applied = await this.applyDraftImpl(prepared.data.sourceId, addOptions.draft ?? prepared.data.draft);
|
|
68
|
-
if (!applied.ok) {
|
|
69
|
-
return fail(applied.errors, [...prepared.warnings, ...applied.warnings]);
|
|
70
|
-
}
|
|
71
|
-
return ok({
|
|
72
|
-
...prepared.data,
|
|
73
|
-
draft: applied.data.draft,
|
|
74
|
-
projected: true,
|
|
75
|
-
}, [...prepared.warnings, ...applied.warnings]);
|
|
76
|
-
}
|
|
77
|
-
async prepareAddSourceImpl(locator, options) {
|
|
78
|
-
const addOptions = options ?? {};
|
|
79
|
-
const result = await this.sourceService.addSource(locator, addOptions);
|
|
80
|
-
if (!result.ok) {
|
|
81
|
-
return fail(result.errors, result.warnings);
|
|
82
|
-
}
|
|
83
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
84
|
-
const source = manifest.sources.find((item) => item.id === result.data.manifest.id);
|
|
85
|
-
if (!source) {
|
|
86
|
-
return fail({
|
|
87
|
-
code: "SOURCE_NOT_FOUND",
|
|
88
|
-
message: `Skills group id '${result.data.manifest.id}' is not registered.`,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const requestedPath = this.normalizeRequestedPath(source.requestedPath);
|
|
92
|
-
if (requestedPath) {
|
|
93
|
-
source.requestedPath = requestedPath;
|
|
94
|
-
result.data.manifest.requestedPath = requestedPath;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
delete source.requestedPath;
|
|
98
|
-
delete result.data.manifest.requestedPath;
|
|
99
|
-
}
|
|
100
|
-
const sourceLeafs = lockFile.leafInventory.filter((leaf) => leaf.sourceId === source.id);
|
|
101
|
-
const availableTargets = addOptions.skipTargetDetection
|
|
102
|
-
? []
|
|
103
|
-
: await this.getAvailableTargets();
|
|
104
|
-
const preparedDraft = this.buildAddDraft(sourceLeafs, requestedPath, availableTargets, addOptions);
|
|
105
|
-
if (!preparedDraft.ok) {
|
|
106
|
-
await this.rollbackPreparedSourceInternal(source.id);
|
|
107
|
-
return fail(preparedDraft.errors, [...result.warnings, ...preparedDraft.warnings]);
|
|
108
|
-
}
|
|
109
|
-
source.selectionMode =
|
|
110
|
-
addOptions.selectionMode ??
|
|
111
|
-
(preparedDraft.data.selectedLeafIds.length >= sourceLeafs.length && sourceLeafs.length > 0
|
|
112
|
-
? "all"
|
|
113
|
-
: "partial");
|
|
114
|
-
result.data.manifest.selectionMode = source.selectionMode;
|
|
115
|
-
manifest.bindings[source.id] = { targets: {} };
|
|
116
|
-
await this.store.writeState(manifest, lockFile);
|
|
117
|
-
const warnings = [...result.warnings];
|
|
118
|
-
if (requestedPath &&
|
|
119
|
-
!addOptions.skillNames?.length &&
|
|
120
|
-
!addOptions.draft &&
|
|
121
|
-
preparedDraft.data.selectedLeafIds.length < sourceLeafs.length) {
|
|
122
|
-
warnings.push({
|
|
123
|
-
code: "ADD_SELECTION_PRESELECTED",
|
|
124
|
-
message: `Preselected ${preparedDraft.data.selectedLeafIds.length} of ${sourceLeafs.length} ` +
|
|
125
|
-
`skill${sourceLeafs.length === 1 ? "" : "s"} under '${requestedPath}'; ` +
|
|
126
|
-
"the full skills group was imported.",
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return ok({
|
|
130
|
-
...result.data,
|
|
131
|
-
sourceId: source.id,
|
|
132
|
-
availableTargets,
|
|
133
|
-
draft: preparedDraft.data,
|
|
134
|
-
leafs: sourceLeafs,
|
|
135
|
-
projected: false,
|
|
136
|
-
}, warnings);
|
|
137
|
-
}
|
|
138
|
-
async rollbackPreparedSource(sourceId) {
|
|
139
|
-
return this.runSerializedMutation(() => this.rollbackPreparedSourceInternal(sourceId));
|
|
140
|
-
}
|
|
141
|
-
async findSkills(query) {
|
|
142
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
143
|
-
const normalizedQuery = this.normalizeSearchQuery(query);
|
|
144
|
-
const warnings = [];
|
|
145
|
-
const localKeys = new Set();
|
|
146
|
-
const candidates = [];
|
|
147
|
-
let remoteSearchSucceeded = false;
|
|
148
|
-
for (const candidate of this.buildLocalCandidates(normalizedQuery, manifest, lockFile)) {
|
|
149
|
-
candidates.push(candidate);
|
|
150
|
-
localKeys.add(this.getCandidateKey(candidate));
|
|
151
|
-
}
|
|
152
|
-
const builtinResults = await Promise.all(getBuiltinGitSources().map(async (builtin) => {
|
|
153
|
-
try {
|
|
154
|
-
const sourceId = deriveSourceId(builtin.locator);
|
|
155
|
-
const displayName = deriveDisplayName(builtin.locator);
|
|
156
|
-
const search = await this.searchBuiltinGitSource(builtin.locator, builtin.branch, sourceId, displayName, normalizedQuery);
|
|
157
|
-
return {
|
|
158
|
-
ok: true,
|
|
159
|
-
candidates: search.candidates,
|
|
160
|
-
warnings: search.warnings,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
return {
|
|
165
|
-
ok: false,
|
|
166
|
-
warning: {
|
|
167
|
-
code: "BUILTIN_SOURCE_UNAVAILABLE",
|
|
168
|
-
message: `Unable to refresh built-in source '${builtin.locator}': ${String(error)}`,
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
}));
|
|
173
|
-
for (const result of builtinResults) {
|
|
174
|
-
if (!result.ok) {
|
|
175
|
-
warnings.push(result.warning);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
warnings.push(...result.warnings);
|
|
179
|
-
remoteSearchSucceeded = true;
|
|
180
|
-
for (const candidate of result.candidates) {
|
|
181
|
-
if (localKeys.has(this.getCandidateKey(candidate))) {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
candidates.push(candidate);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
const results = await searchClawHubSkills(normalizedQuery, 8);
|
|
189
|
-
remoteSearchSucceeded = true;
|
|
190
|
-
for (const result of results) {
|
|
191
|
-
candidates.push({
|
|
192
|
-
id: `clawhub:${result.slug}`,
|
|
193
|
-
title: result.title,
|
|
194
|
-
description: result.title,
|
|
195
|
-
source: "clawhub",
|
|
196
|
-
sourceLabel: "ClawHub",
|
|
197
|
-
sourceId: deriveSourceId(`clawhub:${result.slug}`),
|
|
198
|
-
sourceKind: "clawhub",
|
|
199
|
-
locator: `clawhub:${result.slug}`,
|
|
200
|
-
installed: manifest.sources.some((source) => source.id === deriveSourceId(`clawhub:${result.slug}`)),
|
|
201
|
-
action: {
|
|
202
|
-
type: "add-clawhub",
|
|
203
|
-
slug: result.slug,
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
warnings.push({
|
|
210
|
-
code: "CLAWHUB_SEARCH_FAILED",
|
|
211
|
-
message: `Unable to search ClawHub: ${String(error)}`,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
if (candidates.length === 0 && !remoteSearchSucceeded) {
|
|
215
|
-
return fail({
|
|
216
|
-
code: "FIND_UNAVAILABLE",
|
|
217
|
-
message: "Unable to search built-in sources or ClawHub.",
|
|
218
|
-
}, warnings);
|
|
219
|
-
}
|
|
220
|
-
candidates.sort((left, right) => this.compareCandidates(left, right, normalizedQuery));
|
|
221
|
-
return ok({ candidates }, warnings);
|
|
222
|
-
}
|
|
223
|
-
async listWorkflows() {
|
|
224
|
-
return this.runSerializedMutation(() => this.listWorkflowsImpl());
|
|
225
|
-
}
|
|
226
|
-
async listWorkflowsImpl() {
|
|
227
|
-
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
228
|
-
if (!pruned.ok) {
|
|
229
|
-
return fail(pruned.errors, pruned.warnings);
|
|
230
|
-
}
|
|
231
|
-
const reconciled = await this.sourceService.reconcileInventory(undefined, {
|
|
232
|
-
force: true,
|
|
233
|
-
});
|
|
234
|
-
if (!reconciled.ok) {
|
|
235
|
-
return fail(reconciled.errors, reconciled.warnings);
|
|
236
|
-
}
|
|
237
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
238
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
239
|
-
return ok({
|
|
240
|
-
summaries: this.workflowService.getSummaries(manifest, lockFile),
|
|
241
|
-
}, pruned.warnings);
|
|
242
|
-
}
|
|
243
|
-
async getConfigData() {
|
|
244
|
-
return this.runSerializedMutation(() => this.getConfigDataImpl());
|
|
245
|
-
}
|
|
246
|
-
async getConfigDataImpl() {
|
|
247
|
-
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
248
|
-
if (!pruned.ok) {
|
|
249
|
-
return fail(pruned.errors, pruned.warnings);
|
|
250
|
-
}
|
|
251
|
-
const reconciled = await this.sourceService.reconcileInventory(undefined, {
|
|
252
|
-
force: true,
|
|
253
|
-
});
|
|
254
|
-
if (!reconciled.ok) {
|
|
255
|
-
return fail(reconciled.errors, reconciled.warnings);
|
|
256
|
-
}
|
|
257
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
258
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
259
|
-
return ok({
|
|
260
|
-
manifest,
|
|
261
|
-
lockFile,
|
|
262
|
-
summaries: this.workflowService.getSummaries(manifest, lockFile),
|
|
263
|
-
}, pruned.warnings);
|
|
264
|
-
}
|
|
265
|
-
async bootstrapWorkspaceState(onEvent) {
|
|
266
|
-
return this.runSerializedMutation(() => this.bootstrapWorkspaceStateImpl(onEvent));
|
|
267
|
-
}
|
|
268
|
-
async bootstrapWorkspaceStateImpl(onEvent) {
|
|
269
|
-
const boot = await this.configCoordinator.bootstrapWorkspaceState(onEvent);
|
|
270
|
-
if (!boot.ok) {
|
|
271
|
-
return fail(boot.errors, boot.warnings);
|
|
272
|
-
}
|
|
273
|
-
return ok({
|
|
274
|
-
availableTargets: boot.data.availableTargets,
|
|
275
|
-
manifest: boot.data.manifest,
|
|
276
|
-
lockFile: boot.data.lockFile,
|
|
277
|
-
summaries: boot.data.summaries,
|
|
278
|
-
initialDrafts: boot.data.initialDrafts,
|
|
279
|
-
audit: boot.data.audit,
|
|
280
|
-
importedSourceIds: [],
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
async getAvailableTargets() {
|
|
284
|
-
const adapters = createChannelAdapters();
|
|
285
|
-
const availableTargets = [];
|
|
286
|
-
for (const adapter of adapters) {
|
|
287
|
-
const detection = await adapter.detect();
|
|
288
|
-
if (detection.available) {
|
|
289
|
-
availableTargets.push(adapter.target);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return availableTargets;
|
|
293
|
-
}
|
|
294
|
-
async previewDraft(sourceId, draft) {
|
|
295
|
-
// config TUI state flow:
|
|
296
|
-
// draft -> previewDraft() -> plan only
|
|
297
|
-
// draft -> applyDraft() -> plan + filesystem + manifest/lock writes
|
|
298
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
299
|
-
this.normalizeBindings(manifest, lockFile);
|
|
300
|
-
const prepared = this.prepareManifestForDraft(manifest, lockFile, sourceId, draft);
|
|
301
|
-
const plan = await this.planForAffectedSources(prepared.manifest, lockFile, sourceId);
|
|
302
|
-
if (!plan.ok) {
|
|
303
|
-
return fail(plan.errors, [...prepared.warnings, ...plan.warnings]);
|
|
304
|
-
}
|
|
305
|
-
return ok({ plan: plan.data, manifest: prepared.manifest, lockFile }, [...prepared.warnings, ...plan.warnings]);
|
|
306
|
-
}
|
|
307
|
-
async applyDraft(sourceId, draft) {
|
|
308
|
-
return this.runSerializedMutation(() => this.applyDraftImpl(sourceId, draft));
|
|
309
|
-
}
|
|
310
|
-
async applyDraftImpl(sourceId, draft) {
|
|
311
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
312
|
-
this.normalizeBindings(manifest, lockFile);
|
|
313
|
-
const prepared = this.prepareManifestForDraft(manifest, lockFile, sourceId, draft);
|
|
314
|
-
const plan = await this.planForAffectedSources(prepared.manifest, lockFile, sourceId);
|
|
315
|
-
if (!plan.ok) {
|
|
316
|
-
return fail(plan.errors, [...prepared.warnings, ...plan.warnings]);
|
|
317
|
-
}
|
|
318
|
-
const applyResult = await this.applier.applyPlan(lockFile, plan.data.actions);
|
|
319
|
-
await this.store.writeState(prepared.manifest, lockFile);
|
|
320
|
-
if (!applyResult.ok) {
|
|
321
|
-
return fail(applyResult.errors, [...prepared.warnings, ...plan.warnings, ...applyResult.warnings]);
|
|
322
|
-
}
|
|
323
|
-
return ok({ actions: plan.data.actions, draft: prepared.draft }, [...prepared.warnings, ...plan.warnings, ...applyResult.warnings]);
|
|
324
|
-
}
|
|
325
|
-
async updateSources(sourceIds) {
|
|
326
|
-
return this.runSerializedMutation(() => this.updateSourcesImpl(sourceIds));
|
|
327
|
-
}
|
|
328
|
-
async updateSourcesImpl(sourceIds) {
|
|
329
|
-
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
330
|
-
if (!pruned.ok) {
|
|
331
|
-
return fail(pruned.errors, pruned.warnings);
|
|
332
|
-
}
|
|
333
|
-
const requestedIds = sourceIds?.filter((sourceId) => !pruned.data.removedSourceIds.includes(sourceId));
|
|
334
|
-
if (sourceIds?.length && requestedIds?.length === 0) {
|
|
335
|
-
return ok({ updated: [] }, pruned.warnings);
|
|
336
|
-
}
|
|
337
|
-
const updated = await this.sourceService.updateSources(requestedIds);
|
|
338
|
-
if (!updated.ok) {
|
|
339
|
-
return updated;
|
|
340
|
-
}
|
|
341
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
342
|
-
this.applySourceUpdateResults(manifest, lockFile, updated.data.updated);
|
|
343
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
344
|
-
const planSourceIds = manifest.sources
|
|
345
|
-
.map((source) => source.id)
|
|
346
|
-
.filter((id) => updated.data.updated.some((item) => item.sourceId === id) ||
|
|
347
|
-
this.hasActiveTargets(manifest, id) ||
|
|
348
|
-
lockFile.deployments.some((deployment) => deployment.sourceId === id));
|
|
349
|
-
const planned = await this.planAndApplySources(manifest, lockFile, planSourceIds);
|
|
350
|
-
if (!planned.ok) {
|
|
351
|
-
return fail(planned.errors, [...pruned.warnings, ...updated.warnings, ...planned.warnings]);
|
|
352
|
-
}
|
|
353
|
-
await this.store.writeState(manifest, lockFile);
|
|
354
|
-
return ok(updated.data, [...pruned.warnings, ...updated.warnings, ...planned.warnings]);
|
|
355
|
-
}
|
|
356
|
-
async doctor() {
|
|
357
|
-
return this.runSerializedMutation(() => this.doctorImpl());
|
|
358
|
-
}
|
|
359
|
-
async doctorImpl() {
|
|
360
|
-
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
361
|
-
if (!pruned.ok) {
|
|
362
|
-
return fail(pruned.errors, pruned.warnings);
|
|
363
|
-
}
|
|
364
|
-
const reconciled = await this.sourceService.reconcileInventory();
|
|
365
|
-
if (!reconciled.ok) {
|
|
366
|
-
return fail(reconciled.errors, reconciled.warnings);
|
|
367
|
-
}
|
|
368
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
369
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
370
|
-
const doctor = await this.doctorService.run(manifest, lockFile);
|
|
371
|
-
if (!doctor.ok) {
|
|
372
|
-
return doctor;
|
|
373
|
-
}
|
|
374
|
-
return ok(doctor.data, [...pruned.warnings, ...doctor.warnings]);
|
|
375
|
-
}
|
|
376
|
-
async repairTargets(sourceIds) {
|
|
377
|
-
return this.runSerializedMutation(() => this.repairTargetsImpl(sourceIds));
|
|
378
|
-
}
|
|
379
|
-
async repairTargetsImpl(sourceIds) {
|
|
380
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
381
|
-
this.normalizeBindings(manifest, lockFile);
|
|
382
|
-
const requestedIds = sourceIds?.length
|
|
383
|
-
? sourceIds
|
|
384
|
-
: manifest.sources.map((source) => source.id);
|
|
385
|
-
for (const sourceId of requestedIds) {
|
|
386
|
-
if (!manifest.sources.some((source) => source.id === sourceId)) {
|
|
387
|
-
return fail({
|
|
388
|
-
code: "SOURCE_NOT_FOUND",
|
|
389
|
-
message: `Skills group id '${sourceId}' is not registered.`,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const planSourceIds = requestedIds.filter((sourceId) => this.hasActiveTargets(manifest, sourceId) ||
|
|
394
|
-
lockFile.deployments.some((deployment) => deployment.sourceId === sourceId));
|
|
395
|
-
const planned = await this.planAndApplySources(manifest, lockFile, planSourceIds);
|
|
396
|
-
if (!planned.ok) {
|
|
397
|
-
return fail(planned.errors, planned.warnings);
|
|
398
|
-
}
|
|
399
|
-
await this.store.writeState(manifest, lockFile);
|
|
400
|
-
return ok({ actions: planned.data.actions }, planned.warnings);
|
|
401
|
-
}
|
|
402
|
-
async repairSource(sourceIds) {
|
|
403
|
-
return this.runSerializedMutation(() => this.repairSourceImpl(sourceIds));
|
|
404
|
-
}
|
|
405
|
-
async repairSourceImpl(sourceIds) {
|
|
406
|
-
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
407
|
-
if (!pruned.ok) {
|
|
408
|
-
return fail(pruned.errors, pruned.warnings);
|
|
409
|
-
}
|
|
410
|
-
const requestedIds = sourceIds?.filter((sourceId) => !pruned.data.removedSourceIds.includes(sourceId));
|
|
411
|
-
if (sourceIds?.length && requestedIds?.length === 0) {
|
|
412
|
-
return ok({ updated: [] }, pruned.warnings);
|
|
413
|
-
}
|
|
414
|
-
const repaired = await this.sourceService.updateSources(requestedIds);
|
|
415
|
-
if (!repaired.ok) {
|
|
416
|
-
return repaired;
|
|
417
|
-
}
|
|
418
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
419
|
-
this.applySourceUpdateResults(manifest, lockFile, repaired.data.updated);
|
|
420
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
421
|
-
await this.store.writeState(manifest, lockFile);
|
|
422
|
-
return ok(repaired.data, [...pruned.warnings, ...repaired.warnings]);
|
|
423
|
-
}
|
|
424
|
-
async repairState(sourceIds) {
|
|
425
|
-
return this.runSerializedMutation(() => this.repairStateImpl(sourceIds));
|
|
426
|
-
}
|
|
427
|
-
async repairStateImpl(sourceIds) {
|
|
428
|
-
const pruned = await this.pruneMissingCheckoutsImpl();
|
|
429
|
-
if (!pruned.ok) {
|
|
430
|
-
return fail(pruned.errors, pruned.warnings);
|
|
431
|
-
}
|
|
432
|
-
const requestedIds = sourceIds?.filter((sourceId) => !pruned.data.removedSourceIds.includes(sourceId));
|
|
433
|
-
if (sourceIds?.length && requestedIds?.length === 0) {
|
|
434
|
-
return ok({ repairedSourceIds: [], removedDeploymentCount: 0 }, pruned.warnings);
|
|
435
|
-
}
|
|
436
|
-
const reconciled = await this.sourceService.reconcileInventory(requestedIds, {
|
|
437
|
-
force: true,
|
|
438
|
-
});
|
|
439
|
-
if (!reconciled.ok) {
|
|
440
|
-
return fail(reconciled.errors, [...pruned.warnings, ...reconciled.warnings]);
|
|
441
|
-
}
|
|
442
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
443
|
-
await this.persistNormalizedBindings(manifest, lockFile);
|
|
444
|
-
const removedDeploymentCount = await this.rebuildDeploymentState(manifest, lockFile, requestedIds);
|
|
445
|
-
await this.store.writeState(manifest, lockFile);
|
|
446
|
-
return ok({
|
|
447
|
-
repairedSourceIds: reconciled.data.updatedSourceIds,
|
|
448
|
-
removedDeploymentCount,
|
|
449
|
-
}, [...pruned.warnings, ...reconciled.warnings]);
|
|
450
|
-
}
|
|
451
|
-
async uninstall(sourceIds) {
|
|
452
|
-
return this.runSerializedMutation(() => this.uninstallImpl(sourceIds));
|
|
453
|
-
}
|
|
454
|
-
async uninstallImpl(sourceIds) {
|
|
455
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
456
|
-
const warnings = [];
|
|
457
|
-
const targetRoots = await this.getTargetRootMap();
|
|
458
|
-
const removedRefs = sourceIds
|
|
459
|
-
.map((sourceId) => manifest.sources.find((source) => source.id === sourceId))
|
|
460
|
-
.filter((source) => Boolean(source));
|
|
461
|
-
for (const sourceId of sourceIds) {
|
|
462
|
-
const deployments = lockFile.deployments.filter((deployment) => deployment.sourceId === sourceId);
|
|
463
|
-
for (const deployment of deployments) {
|
|
464
|
-
if (!(await pathExists(deployment.targetPath))) {
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (!this.isPathInsideManagedTargetRoot(deployment.target, deployment.targetPath, targetRoots, deployment.targetRootPath)) {
|
|
468
|
-
warnings.push(`Refusing to remove unmanaged target path ${deployment.targetPath}.`);
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
try {
|
|
472
|
-
await removePath(deployment.targetPath);
|
|
473
|
-
}
|
|
474
|
-
catch (error) {
|
|
475
|
-
warnings.push(`Unable to remove ${deployment.targetPath}: ${String(error)}`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (warnings.length > 0) {
|
|
480
|
-
return fail({
|
|
481
|
-
code: "GROUP_DELETE_INCOMPLETE",
|
|
482
|
-
message: `Unable to fully delete ${warnings.length} managed path${warnings.length === 1 ? "" : "s"}.`,
|
|
483
|
-
}, warnings.map((message) => ({
|
|
484
|
-
code: "GROUP_DELETE_PATH_FAILED",
|
|
485
|
-
message,
|
|
486
|
-
})));
|
|
487
|
-
}
|
|
488
|
-
let removed;
|
|
489
|
-
try {
|
|
490
|
-
removed = await this.sourceService.removeSource(sourceIds);
|
|
491
|
-
}
|
|
492
|
-
catch (error) {
|
|
493
|
-
return fail({
|
|
494
|
-
code: "GROUP_DELETE_INCOMPLETE",
|
|
495
|
-
message: `Unable to fully delete selected skills groups: ${String(error)}`,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
if (!removed.ok) {
|
|
499
|
-
return fail(removed.errors, removed.warnings);
|
|
500
|
-
}
|
|
501
|
-
return ok({ removed: removed.data.removed, removedRefs, warnings });
|
|
502
|
-
}
|
|
503
|
-
bindingFromDraft(draft) {
|
|
504
|
-
const targets = {};
|
|
505
|
-
for (const target of draft.enabledTargets) {
|
|
506
|
-
targets[target] = {
|
|
507
|
-
enabled: true,
|
|
508
|
-
leafIds: [...draft.selectedLeafIds],
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
selectedLeafIds: [...draft.selectedLeafIds],
|
|
513
|
-
targets,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
async pruneMissingCheckoutsImpl() {
|
|
517
|
-
const { manifest, lockFile } = await this.store.readState();
|
|
518
|
-
const removedSourceIds = [];
|
|
519
|
-
const warnings = [];
|
|
520
|
-
const targetRoots = await this.getTargetRootMap();
|
|
521
|
-
for (const source of lockFile.sources) {
|
|
522
|
-
if (await pathExists(source.checkoutPath)) {
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
removedSourceIds.push(source.id);
|
|
526
|
-
warnings.push({
|
|
527
|
-
code: "SOURCE_CHECKOUT_MISSING",
|
|
528
|
-
message: `Removed ${source.displayName} because checkout is missing at ${source.checkoutPath}.`,
|
|
529
|
-
});
|
|
530
|
-
const deployments = lockFile.deployments.filter((deployment) => deployment.sourceId === source.id);
|
|
531
|
-
for (const deployment of deployments) {
|
|
532
|
-
if (!(await pathExists(deployment.targetPath))) {
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
if (!this.isPathInsideManagedTargetRoot(deployment.target, deployment.targetPath, targetRoots, deployment.targetRootPath)) {
|
|
536
|
-
warnings.push({
|
|
537
|
-
code: "SOURCE_CHECKOUT_PRUNE_SKIPPED",
|
|
538
|
-
message: `Skipped unmanaged deployment path ${deployment.targetPath} while pruning ${source.displayName}.`,
|
|
539
|
-
});
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
await removePath(deployment.targetPath);
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
return fail({
|
|
547
|
-
code: "SOURCE_CHECKOUT_PRUNE_FAILED",
|
|
548
|
-
message: `Unable to clean deployment ${deployment.targetPath}: ${String(error)}`,
|
|
549
|
-
}, warnings);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
if (removedSourceIds.length === 0) {
|
|
554
|
-
return ok({ removedSourceIds: [] });
|
|
555
|
-
}
|
|
556
|
-
manifest.sources = manifest.sources.filter((source) => !removedSourceIds.includes(source.id));
|
|
557
|
-
for (const sourceId of removedSourceIds) {
|
|
558
|
-
delete manifest.bindings[sourceId];
|
|
559
|
-
}
|
|
560
|
-
lockFile.sources = lockFile.sources.filter((source) => !removedSourceIds.includes(source.id));
|
|
561
|
-
lockFile.leafInventory = lockFile.leafInventory.filter((leaf) => !removedSourceIds.includes(leaf.sourceId));
|
|
562
|
-
lockFile.deployments = lockFile.deployments.filter((deployment) => !removedSourceIds.includes(deployment.sourceId));
|
|
563
|
-
await this.store.writeState(manifest, lockFile);
|
|
564
|
-
return ok({ removedSourceIds }, warnings);
|
|
565
|
-
}
|
|
566
|
-
async getTargetRootMap() {
|
|
567
|
-
return new Map(await Promise.all(this.adapters.map(async (adapter) => {
|
|
568
|
-
const detection = await adapter.detect();
|
|
569
|
-
return [adapter.target, detection.rootPath];
|
|
570
|
-
})));
|
|
571
|
-
}
|
|
572
|
-
isPathInsideManagedTargetRoot(target, targetPath, targetRoots, explicitRootPath) {
|
|
573
|
-
return [explicitRootPath, targetRoots.get(target)]
|
|
574
|
-
.filter((value) => Boolean(value))
|
|
575
|
-
.some((rootPath) => isPathInside(rootPath, targetPath));
|
|
576
|
-
}
|
|
577
|
-
async persistNormalizedBindings(manifest, lockFile) {
|
|
578
|
-
if (!this.normalizeBindings(manifest, lockFile)) {
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
await this.store.writeState(manifest, lockFile);
|
|
582
|
-
}
|
|
583
|
-
async runSerializedMutation(task) {
|
|
584
|
-
const run = this.mutationQueue.then(() => this.store.withMutationLock(task), () => this.store.withMutationLock(task));
|
|
585
|
-
this.mutationQueue = run.then(() => undefined, () => undefined);
|
|
586
|
-
return run;
|
|
587
|
-
}
|
|
588
|
-
normalizeBindings(manifest, lockFile) {
|
|
589
|
-
let changed = false;
|
|
590
|
-
for (const source of manifest.sources) {
|
|
591
|
-
const currentBinding = manifest.bindings[source.id] ?? { targets: {} };
|
|
592
|
-
const normalizedDraft = this.draftFromBinding(source.id, currentBinding, lockFile);
|
|
593
|
-
const normalizedBinding = this.bindingFromDraft(normalizedDraft);
|
|
594
|
-
if (JSON.stringify(currentBinding) === JSON.stringify(normalizedBinding)) {
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
manifest.bindings[source.id] = normalizedBinding;
|
|
598
|
-
changed = true;
|
|
599
|
-
}
|
|
600
|
-
return changed;
|
|
601
|
-
}
|
|
602
|
-
draftFromBinding(sourceId, binding, lockFile) {
|
|
603
|
-
const leafIds = new Set(lockFile.leafInventory
|
|
604
|
-
.filter((leaf) => leaf.sourceId === sourceId)
|
|
605
|
-
.map((leaf) => leaf.id));
|
|
606
|
-
const enabledTargets = Object.entries(binding.targets)
|
|
607
|
-
.filter(([, targetBinding]) => targetBinding?.enabled)
|
|
608
|
-
.map(([target]) => target);
|
|
609
|
-
const selectedLeafIds = [
|
|
610
|
-
...new Set((binding.selectedLeafIds && binding.selectedLeafIds.length > 0
|
|
611
|
-
? binding.selectedLeafIds
|
|
612
|
-
: enabledTargets.flatMap((target) => binding.targets[target]?.leafIds ?? []))),
|
|
613
|
-
].filter((leafId) => leafIds.has(leafId));
|
|
614
|
-
return {
|
|
615
|
-
enabledTargets,
|
|
616
|
-
selectedLeafIds,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
selectLeafIdsForRequestedPath(leafs, requestedPath) {
|
|
620
|
-
const normalizedPath = this.normalizeRequestedPath(requestedPath);
|
|
621
|
-
if (!normalizedPath) {
|
|
622
|
-
return leafs.map((leaf) => leaf.id);
|
|
623
|
-
}
|
|
624
|
-
return leafs
|
|
625
|
-
.filter((leaf) => leaf.relativePath === normalizedPath ||
|
|
626
|
-
leaf.relativePath.startsWith(`${normalizedPath}/`))
|
|
627
|
-
.map((leaf) => leaf.id);
|
|
628
|
-
}
|
|
629
|
-
buildAddDraft(sourceLeafs, requestedPath, availableTargets, options) {
|
|
630
|
-
if (options.draft) {
|
|
631
|
-
return ok({
|
|
632
|
-
enabledTargets: [...new Set(options.draft.enabledTargets)],
|
|
633
|
-
selectedLeafIds: [...new Set(options.draft.selectedLeafIds)],
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
const selectedLeafIdsResult = this.resolveSelectedLeafIds(sourceLeafs, requestedPath, options.skillNames);
|
|
637
|
-
if (!selectedLeafIdsResult.ok) {
|
|
638
|
-
return fail(selectedLeafIdsResult.errors, selectedLeafIdsResult.warnings);
|
|
639
|
-
}
|
|
640
|
-
const enabledTargetsResult = this.resolveRequestedTargets(availableTargets, options.agentTargets ?? options.enabledTargets);
|
|
641
|
-
if (!enabledTargetsResult.ok) {
|
|
642
|
-
return fail(enabledTargetsResult.errors, enabledTargetsResult.warnings);
|
|
643
|
-
}
|
|
644
|
-
return ok({
|
|
645
|
-
enabledTargets: enabledTargetsResult.data,
|
|
646
|
-
selectedLeafIds: selectedLeafIdsResult.data,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
resolveSelectedLeafIds(sourceLeafs, requestedPath, skillNames) {
|
|
650
|
-
if (!skillNames || skillNames.length === 0) {
|
|
651
|
-
return ok(this.selectLeafIdsForRequestedPath(sourceLeafs, requestedPath));
|
|
652
|
-
}
|
|
653
|
-
const requested = [...new Set(skillNames.map((skillName) => skillName.trim()).filter(Boolean))];
|
|
654
|
-
const matchedLeafIds = [];
|
|
655
|
-
for (const selector of requested) {
|
|
656
|
-
const relativePathMatches = sourceLeafs.filter((leaf) => leaf.relativePath === selector);
|
|
657
|
-
if (relativePathMatches.length === 1) {
|
|
658
|
-
matchedLeafIds.push(relativePathMatches[0].id);
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
if (relativePathMatches.length > 1) {
|
|
662
|
-
return fail({
|
|
663
|
-
code: "ADD_SKILL_SELECTOR_AMBIGUOUS",
|
|
664
|
-
message: `Skill selector '${selector}' is ambiguous. Use a unique relative path.`,
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
const fallbackMatches = sourceLeafs.filter((leaf) => leaf.linkName === selector || leaf.name === selector);
|
|
668
|
-
if (fallbackMatches.length === 1) {
|
|
669
|
-
matchedLeafIds.push(fallbackMatches[0].id);
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
if (fallbackMatches.length > 1) {
|
|
673
|
-
return fail({
|
|
674
|
-
code: "ADD_SKILL_SELECTOR_AMBIGUOUS",
|
|
675
|
-
message: `Skill selector '${selector}' is ambiguous. ` +
|
|
676
|
-
`Use a relative path such as '${fallbackMatches[0].relativePath}'.`,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
return fail({
|
|
680
|
-
code: "ADD_SKILL_NOT_FOUND",
|
|
681
|
-
message: `Unable to preselect skill(s): ${selector}.`,
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
return ok([...new Set(matchedLeafIds)]);
|
|
685
|
-
}
|
|
686
|
-
resolveRequestedTargets(availableTargets, requestedTargets) {
|
|
687
|
-
if (!requestedTargets?.length) {
|
|
688
|
-
return ok([...availableTargets]);
|
|
689
|
-
}
|
|
690
|
-
const available = new Set(availableTargets);
|
|
691
|
-
const unsupported = [...new Set(requestedTargets)].filter((target) => !available.has(target));
|
|
692
|
-
if (unsupported.length > 0) {
|
|
693
|
-
return fail({
|
|
694
|
-
code: "ADD_AGENT_NOT_AVAILABLE",
|
|
695
|
-
message: `Unknown or unavailable agent(s): ${unsupported.join(", ")}.`,
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
return ok([...new Set(requestedTargets)]);
|
|
699
|
-
}
|
|
700
|
-
normalizeRequestedPath(requestedPath) {
|
|
701
|
-
if (!requestedPath) {
|
|
702
|
-
return undefined;
|
|
703
|
-
}
|
|
704
|
-
const normalized = requestedPath.trim().replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
705
|
-
return normalized.length > 0 && normalized !== "." ? normalized : undefined;
|
|
706
|
-
}
|
|
707
|
-
async rollbackPreparedSourceInternal(sourceId) {
|
|
708
|
-
const { lockFile, manifest } = await this.store.readState();
|
|
709
|
-
if (!manifest.sources.some((source) => source.id === sourceId)) {
|
|
710
|
-
return fail({
|
|
711
|
-
code: "SOURCE_NOT_FOUND",
|
|
712
|
-
message: `Skills group id '${sourceId}' is not registered.`,
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
if (lockFile.deployments.some((deployment) => deployment.sourceId === sourceId)) {
|
|
716
|
-
return fail({
|
|
717
|
-
code: "ADD_ROLLBACK_HAS_DEPLOYMENTS",
|
|
718
|
-
message: `Unable to roll back skills group id '${sourceId}' because deployments already exist.`,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
return this.sourceService.removeSource([sourceId]);
|
|
722
|
-
}
|
|
723
|
-
prepareManifestForDraft(manifest, lockFile, sourceId, draft) {
|
|
724
|
-
manifest.bindings[sourceId] = this.bindingFromDraft(draft);
|
|
725
|
-
const source = manifest.sources.find((item) => item.id === sourceId);
|
|
726
|
-
if (source) {
|
|
727
|
-
const sourceLeafCount = lockFile.leafInventory.filter((leaf) => leaf.sourceId === sourceId).length;
|
|
728
|
-
source.selectionMode =
|
|
729
|
-
draft.selectedLeafIds.length >= sourceLeafCount && sourceLeafCount > 0
|
|
730
|
-
? "all"
|
|
731
|
-
: "partial";
|
|
732
|
-
}
|
|
733
|
-
const conflictingLeafIds = this.findExactDuplicateLeafSelections(manifest, lockFile, sourceId, draft.enabledTargets);
|
|
734
|
-
const normalizedDraft = {
|
|
735
|
-
enabledTargets: [...draft.enabledTargets],
|
|
736
|
-
selectedLeafIds: draft.selectedLeafIds.filter((leafId) => !conflictingLeafIds.has(leafId)),
|
|
737
|
-
};
|
|
738
|
-
manifest.bindings[sourceId] = this.bindingFromDraft(normalizedDraft);
|
|
739
|
-
const warnings = [...conflictingLeafIds].map((leafId) => ({
|
|
740
|
-
code: "DUPLICATE_LEAF_SELECTION_SKIPPED",
|
|
741
|
-
message: `${lockFile.leafInventory.find((leaf) => leaf.id === leafId)?.linkName ?? leafId} skipped because an identical skill is already selected in another skills group.`,
|
|
742
|
-
}));
|
|
743
|
-
return {
|
|
744
|
-
manifest,
|
|
745
|
-
draft: normalizedDraft,
|
|
746
|
-
warnings,
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
findExactDuplicateLeafSelections(manifest, lockFile, currentSourceId, enabledTargets) {
|
|
750
|
-
const conflictingKeys = new Set();
|
|
751
|
-
for (const source of manifest.sources) {
|
|
752
|
-
if (source.id === currentSourceId) {
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
const binding = manifest.bindings[source.id];
|
|
756
|
-
if (!binding) {
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
for (const target of enabledTargets) {
|
|
760
|
-
const targetBinding = binding.targets[target];
|
|
761
|
-
if (!targetBinding?.enabled) {
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
for (const leafId of targetBinding.leafIds) {
|
|
765
|
-
const leaf = lockFile.leafInventory.find((item) => item.id === leafId);
|
|
766
|
-
if (!leaf) {
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
conflictingKeys.add(this.getExactDuplicateKey(leaf));
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
const currentLeafs = lockFile.leafInventory.filter((leaf) => leaf.sourceId === currentSourceId);
|
|
774
|
-
return new Set(currentLeafs
|
|
775
|
-
.filter((leaf) => conflictingKeys.has(this.getExactDuplicateKey(leaf)))
|
|
776
|
-
.map((leaf) => leaf.id));
|
|
777
|
-
}
|
|
778
|
-
getExactDuplicateKey(leaf) {
|
|
779
|
-
return `${leaf.linkName}\n${leaf.name}\n${leaf.description}`;
|
|
780
|
-
}
|
|
781
|
-
async planForAffectedSources(manifest, lockFile, primarySourceId) {
|
|
782
|
-
const sourceIds = manifest.sources
|
|
783
|
-
.map((source) => source.id)
|
|
784
|
-
.filter((sourceId) => sourceId === primarySourceId || this.hasActiveTargets(manifest, sourceId));
|
|
785
|
-
return this.planForSources(manifest, lockFile, sourceIds);
|
|
786
|
-
}
|
|
787
|
-
async planForSources(manifest, lockFile, sourceIds) {
|
|
788
|
-
const uniqueSourceIds = [...new Set(sourceIds)];
|
|
789
|
-
const actions = [];
|
|
790
|
-
const warnings = [];
|
|
791
|
-
for (const sourceId of uniqueSourceIds) {
|
|
792
|
-
const plan = await this.planner.planForSource(sourceId, manifest, lockFile);
|
|
793
|
-
if (!plan.ok) {
|
|
794
|
-
return fail(plan.errors, [...warnings, ...plan.warnings]);
|
|
795
|
-
}
|
|
796
|
-
actions.push(...plan.data.actions);
|
|
797
|
-
warnings.push(...plan.warnings);
|
|
798
|
-
}
|
|
799
|
-
return ok({
|
|
800
|
-
actions,
|
|
801
|
-
warnings,
|
|
802
|
-
blocked: actions.filter((action) => action.kind === "blocked"),
|
|
803
|
-
}, warnings);
|
|
804
|
-
}
|
|
805
|
-
async planAndApplySources(manifest, lockFile, sourceIds) {
|
|
806
|
-
const planned = await this.planForSources(manifest, lockFile, sourceIds);
|
|
807
|
-
if (!planned.ok) {
|
|
808
|
-
return fail(planned.errors, planned.warnings);
|
|
809
|
-
}
|
|
810
|
-
const applyResult = await this.applier.applyPlan(lockFile, planned.data.actions);
|
|
811
|
-
if (!applyResult.ok) {
|
|
812
|
-
return fail(applyResult.errors, [...planned.warnings, ...applyResult.warnings]);
|
|
813
|
-
}
|
|
814
|
-
return ok({ actions: planned.data.actions }, [...planned.warnings, ...applyResult.warnings]);
|
|
815
|
-
}
|
|
816
|
-
async rebuildDeploymentState(manifest, lockFile, sourceIds) {
|
|
817
|
-
const requested = sourceIds?.length ? new Set(sourceIds) : undefined;
|
|
818
|
-
const previousDeployments = lockFile.deployments;
|
|
819
|
-
const previousCount = previousDeployments.length;
|
|
820
|
-
const previousByKey = new Map(previousDeployments.map((deployment) => [
|
|
821
|
-
this.getDeploymentKey(deployment.sourceId, deployment.leafId, deployment.target),
|
|
822
|
-
deployment,
|
|
823
|
-
]));
|
|
824
|
-
const nextDeployments = previousDeployments.filter((deployment) => requested ? !requested.has(deployment.sourceId) : false);
|
|
825
|
-
const adapters = createChannelAdapters();
|
|
826
|
-
const detectionCache = new Map();
|
|
827
|
-
const projectedNameCache = new Map();
|
|
828
|
-
for (const source of manifest.sources) {
|
|
829
|
-
if (requested && !requested.has(source.id)) {
|
|
830
|
-
continue;
|
|
831
|
-
}
|
|
832
|
-
const binding = manifest.bindings[source.id] ?? { targets: {} };
|
|
833
|
-
for (const adapter of adapters) {
|
|
834
|
-
const targetBinding = binding.targets[adapter.target];
|
|
835
|
-
if (!targetBinding?.enabled) {
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
let detection = detectionCache.get(adapter.target);
|
|
839
|
-
if (!detection) {
|
|
840
|
-
detection = await adapter.detect();
|
|
841
|
-
detectionCache.set(adapter.target, detection);
|
|
842
|
-
}
|
|
843
|
-
for (const leafId of targetBinding.leafIds) {
|
|
844
|
-
const leaf = lockFile.leafInventory.find((candidate) => candidate.sourceId === source.id && candidate.id === leafId);
|
|
845
|
-
if (!leaf) {
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
const existing = previousByKey.get(this.getDeploymentKey(source.id, leaf.id, adapter.target));
|
|
849
|
-
if (!detection.available) {
|
|
850
|
-
if (existing) {
|
|
851
|
-
nextDeployments.push({
|
|
852
|
-
...existing,
|
|
853
|
-
contentHash: leaf.contentHash,
|
|
854
|
-
status: "active",
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
let projectedLinkNames = projectedNameCache.get(adapter.target);
|
|
860
|
-
if (!projectedLinkNames) {
|
|
861
|
-
projectedLinkNames = this.buildProjectedLinkNameMap(manifest, lockFile, adapter.target);
|
|
862
|
-
projectedNameCache.set(adapter.target, projectedLinkNames);
|
|
863
|
-
}
|
|
864
|
-
const rebuilt = await this.findManagedDeploymentOnDisk(source, leaf, adapter.target, adapter.strategy, detection.rootPath, projectedLinkNames, existing);
|
|
865
|
-
if (!rebuilt) {
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
nextDeployments.push(rebuilt);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
lockFile.deployments = nextDeployments;
|
|
873
|
-
return Math.max(0, previousCount - nextDeployments.length);
|
|
874
|
-
}
|
|
875
|
-
buildProjectedLinkNameMap(manifest, lockFile, target) {
|
|
876
|
-
return resolveProjectedSkillNames(manifest.sources.flatMap((source) => {
|
|
877
|
-
const targetBinding = manifest.bindings[source.id]?.targets[target];
|
|
878
|
-
if (!targetBinding?.enabled) {
|
|
879
|
-
return [];
|
|
880
|
-
}
|
|
881
|
-
return targetBinding.leafIds
|
|
882
|
-
.map((leafId) => lockFile.leafInventory.find((leaf) => leaf.id === leafId))
|
|
883
|
-
.filter((leaf) => Boolean(leaf))
|
|
884
|
-
.map((leaf) => ({
|
|
885
|
-
leafId: leaf.id,
|
|
886
|
-
groupId: source.id,
|
|
887
|
-
groupName: source.displayName,
|
|
888
|
-
groupAuthor: parseGitHubRepo(source.locator)?.owner,
|
|
889
|
-
skillName: leaf.linkName,
|
|
890
|
-
}));
|
|
891
|
-
}));
|
|
892
|
-
}
|
|
893
|
-
async findManagedDeploymentOnDisk(source, leaf, target, strategy, rootPath, projectedLinkNames, existing) {
|
|
894
|
-
const projectedLinkName = projectedLinkNames.get(leaf.id) ?? leaf.linkName;
|
|
895
|
-
const candidatePaths = buildProjectedSkillNameCandidates({
|
|
896
|
-
preferredName: projectedLinkName,
|
|
897
|
-
groupId: source.id,
|
|
898
|
-
groupName: source.displayName,
|
|
899
|
-
groupAuthor: parseGitHubRepo(source.locator)?.owner,
|
|
900
|
-
skillName: leaf.linkName,
|
|
901
|
-
}).map((name) => path.join(rootPath, name));
|
|
902
|
-
if (existing?.targetPath && !candidatePaths.includes(existing.targetPath)) {
|
|
903
|
-
candidatePaths.unshift(existing.targetPath);
|
|
904
|
-
}
|
|
905
|
-
for (const targetPath of candidatePaths) {
|
|
906
|
-
const matches = await this.matchesManagedProjection(strategy, targetPath, leaf);
|
|
907
|
-
if (!matches) {
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
return {
|
|
911
|
-
sourceId: source.id,
|
|
912
|
-
leafId: leaf.id,
|
|
913
|
-
target,
|
|
914
|
-
targetPath,
|
|
915
|
-
targetRootPath: targetPath === existing?.targetPath && existing.targetRootPath
|
|
916
|
-
? existing.targetRootPath
|
|
917
|
-
: rootPath,
|
|
918
|
-
strategy,
|
|
919
|
-
status: "active",
|
|
920
|
-
contentHash: leaf.contentHash,
|
|
921
|
-
appliedAt: existing?.appliedAt ?? new Date().toISOString(),
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
return undefined;
|
|
925
|
-
}
|
|
926
|
-
async matchesManagedProjection(strategy, targetPath, leaf) {
|
|
927
|
-
try {
|
|
928
|
-
const stats = await fs.lstat(targetPath);
|
|
929
|
-
if (strategy === "symlink") {
|
|
930
|
-
if (!stats.isSymbolicLink()) {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
const linked = await fs.readlink(targetPath);
|
|
934
|
-
const resolved = path.resolve(path.dirname(targetPath), linked);
|
|
935
|
-
return resolved === leaf.absolutePath;
|
|
936
|
-
}
|
|
937
|
-
if (!stats.isDirectory()) {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
const onDiskHash = await hashDirectory(targetPath);
|
|
941
|
-
return onDiskHash === leaf.contentHash;
|
|
942
|
-
}
|
|
943
|
-
catch {
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
getDeploymentKey(sourceId, leafId, target) {
|
|
948
|
-
return `${sourceId}\n${leafId}\n${target}`;
|
|
949
|
-
}
|
|
950
|
-
hasActiveTargets(manifest, sourceId) {
|
|
951
|
-
const binding = manifest.bindings[sourceId];
|
|
952
|
-
if (!binding) {
|
|
953
|
-
return false;
|
|
954
|
-
}
|
|
955
|
-
return Object.values(binding.targets).some((target) => target?.enabled);
|
|
956
|
-
}
|
|
957
|
-
buildLocalCandidates(query, manifest, lockFile) {
|
|
958
|
-
return lockFile.leafInventory
|
|
959
|
-
.filter((leaf) => {
|
|
960
|
-
const source = manifest.sources.find((item) => item.id === leaf.sourceId);
|
|
961
|
-
return this.matchesQuery(query, [
|
|
962
|
-
leaf.name,
|
|
963
|
-
leaf.title,
|
|
964
|
-
leaf.relativePath.split("/").pop() ?? "",
|
|
965
|
-
]);
|
|
966
|
-
})
|
|
967
|
-
.map((leaf) => {
|
|
968
|
-
const source = manifest.sources.find((item) => item.id === leaf.sourceId);
|
|
969
|
-
return {
|
|
970
|
-
id: `local:${leaf.id}`,
|
|
971
|
-
title: this.getCandidateTitle(leaf),
|
|
972
|
-
description: leaf.description,
|
|
973
|
-
source: "local",
|
|
974
|
-
sourceLabel: source
|
|
975
|
-
? this.formatSourceLabel(source.locator, source.displayName)
|
|
976
|
-
: leaf.sourceId,
|
|
977
|
-
sourceId: leaf.sourceId,
|
|
978
|
-
sourceKind: source?.kind ?? "git",
|
|
979
|
-
locator: source?.locator ?? leaf.sourceId,
|
|
980
|
-
relativePath: leaf.relativePath,
|
|
981
|
-
installed: true,
|
|
982
|
-
action: { type: "none" },
|
|
983
|
-
};
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
async searchBuiltinGitSource(locator, branch, sourceId, displayName, query) {
|
|
987
|
-
const checkoutPath = this.store.getCatalogCheckoutPath(sourceId);
|
|
988
|
-
if (await pathExists(checkoutPath)) {
|
|
989
|
-
const scanned = await this.inventoryService.scanSource(sourceId, checkoutPath, displayName);
|
|
990
|
-
return {
|
|
991
|
-
candidates: scanned.leafs
|
|
992
|
-
.filter((leaf) => this.matchesQuery(query, [
|
|
993
|
-
leaf.name,
|
|
994
|
-
leaf.relativePath.split("/").pop() ?? "",
|
|
995
|
-
]))
|
|
996
|
-
.map((leaf) => ({
|
|
997
|
-
id: `builtin-git:${leaf.id}`,
|
|
998
|
-
title: leaf.relativePath === "." ? displayName : leaf.relativePath.split("/").pop() ?? displayName,
|
|
999
|
-
description: leaf.relativePath,
|
|
1000
|
-
source: "builtin-git",
|
|
1001
|
-
sourceLabel: this.formatSourceLabel(locator, displayName),
|
|
1002
|
-
sourceId,
|
|
1003
|
-
sourceKind: "git",
|
|
1004
|
-
locator,
|
|
1005
|
-
relativePath: leaf.relativePath,
|
|
1006
|
-
installed: false,
|
|
1007
|
-
action: {
|
|
1008
|
-
type: "add-git",
|
|
1009
|
-
locator,
|
|
1010
|
-
requestedPath: leaf.relativePath,
|
|
1011
|
-
},
|
|
1012
|
-
})),
|
|
1013
|
-
warnings: [],
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
const builtinCatalog = await this.getBuiltinCatalogSkillPaths(locator, branch, sourceId);
|
|
1017
|
-
const skillPaths = builtinCatalog.skillPaths;
|
|
1018
|
-
const matchedPaths = skillPaths
|
|
1019
|
-
.filter((skillFilePath) => this.matchesQuery(query, [
|
|
1020
|
-
skillFilePath.replace(/\/SKILL\.md$/, "").split("/").pop() ?? "",
|
|
1021
|
-
]))
|
|
1022
|
-
.slice(0, 5);
|
|
1023
|
-
return {
|
|
1024
|
-
candidates: matchedPaths.map((skillFilePath) => {
|
|
1025
|
-
const relativePath = skillFilePath.replace(/\/SKILL\.md$/, "").replace(/^SKILL\.md$/, ".");
|
|
1026
|
-
const title = relativePath === "." ? displayName : relativePath.split("/").pop() ?? displayName;
|
|
1027
|
-
return {
|
|
1028
|
-
id: `builtin-git:${sourceId}:${relativePath}`,
|
|
1029
|
-
title,
|
|
1030
|
-
description: relativePath,
|
|
1031
|
-
source: "builtin-git",
|
|
1032
|
-
sourceLabel: this.formatSourceLabel(locator, displayName),
|
|
1033
|
-
sourceId,
|
|
1034
|
-
sourceKind: "git",
|
|
1035
|
-
locator,
|
|
1036
|
-
relativePath,
|
|
1037
|
-
installed: false,
|
|
1038
|
-
action: {
|
|
1039
|
-
type: "add-git",
|
|
1040
|
-
locator,
|
|
1041
|
-
requestedPath: relativePath,
|
|
1042
|
-
},
|
|
1043
|
-
};
|
|
1044
|
-
}),
|
|
1045
|
-
warnings: builtinCatalog.warnings,
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
async getBuiltinCatalogSkillPaths(locator, branch, sourceId) {
|
|
1049
|
-
const indexPath = this.store.getCatalogIndexPath(sourceId);
|
|
1050
|
-
const cached = await readJsonFile(indexPath, {});
|
|
1051
|
-
const cachedSkillPaths = cached.skillPaths ?? [];
|
|
1052
|
-
const cachedUpdatedAt = cached.updatedAt ? Date.parse(cached.updatedAt) : Number.NaN;
|
|
1053
|
-
const cacheFresh = cachedSkillPaths.length > 0 &&
|
|
1054
|
-
Number.isFinite(cachedUpdatedAt) &&
|
|
1055
|
-
Date.now() - cachedUpdatedAt < 1000 * 60 * 60 * 6;
|
|
1056
|
-
if (cacheFresh) {
|
|
1057
|
-
return { skillPaths: cachedSkillPaths, warnings: [] };
|
|
1058
|
-
}
|
|
1059
|
-
try {
|
|
1060
|
-
const skillPaths = await fetchGitHubSkillPaths(locator, branch);
|
|
1061
|
-
await ensureDir(this.store.catalogRoot);
|
|
1062
|
-
await writeJsonFile(indexPath, {
|
|
1063
|
-
locator,
|
|
1064
|
-
branch,
|
|
1065
|
-
skillPaths,
|
|
1066
|
-
updatedAt: new Date().toISOString(),
|
|
1067
|
-
});
|
|
1068
|
-
return { skillPaths, warnings: [] };
|
|
1069
|
-
}
|
|
1070
|
-
catch (error) {
|
|
1071
|
-
if (cachedSkillPaths.length > 0) {
|
|
1072
|
-
return {
|
|
1073
|
-
skillPaths: cachedSkillPaths,
|
|
1074
|
-
warnings: [
|
|
1075
|
-
{
|
|
1076
|
-
code: "BUILTIN_SOURCE_STALE_CACHE_USED",
|
|
1077
|
-
message: `Unable to refresh built-in source '${locator}', using stale cached catalog: ${String(error)}`,
|
|
1078
|
-
},
|
|
1079
|
-
],
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
throw error;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
matchesQuery(query, fields) {
|
|
1086
|
-
const tokens = query.split(/\s+/).filter(Boolean);
|
|
1087
|
-
if (tokens.length === 0) {
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
const haystack = this.normalizeSearchQuery(fields.join("\n"));
|
|
1091
|
-
return tokens.every((token) => haystack.includes(token));
|
|
1092
|
-
}
|
|
1093
|
-
compareCandidates(left, right, query) {
|
|
1094
|
-
const sourceRank = this.getSourceRank(left.source) - this.getSourceRank(right.source);
|
|
1095
|
-
if (sourceRank !== 0) {
|
|
1096
|
-
return sourceRank;
|
|
1097
|
-
}
|
|
1098
|
-
const leftScore = this.getQueryScore(left, query);
|
|
1099
|
-
const rightScore = this.getQueryScore(right, query);
|
|
1100
|
-
if (leftScore !== rightScore) {
|
|
1101
|
-
return rightScore - leftScore;
|
|
1102
|
-
}
|
|
1103
|
-
const leftPath = left.relativePath ?? "";
|
|
1104
|
-
const rightPath = right.relativePath ?? "";
|
|
1105
|
-
return (left.title.localeCompare(right.title) ||
|
|
1106
|
-
left.sourceLabel.localeCompare(right.sourceLabel) ||
|
|
1107
|
-
leftPath.localeCompare(rightPath));
|
|
1108
|
-
}
|
|
1109
|
-
getSourceRank(source) {
|
|
1110
|
-
if (source === "local") {
|
|
1111
|
-
return 0;
|
|
1112
|
-
}
|
|
1113
|
-
if (source === "builtin-git") {
|
|
1114
|
-
return 1;
|
|
1115
|
-
}
|
|
1116
|
-
return 2;
|
|
1117
|
-
}
|
|
1118
|
-
getQueryScore(candidate, query) {
|
|
1119
|
-
const tokens = query.split(/\s+/).filter(Boolean);
|
|
1120
|
-
const titleField = this.normalizeSearchQuery(candidate.title);
|
|
1121
|
-
const pathTail = this.normalizeSearchQuery((candidate.relativePath ?? "").split("/").pop() ?? "");
|
|
1122
|
-
const fields = [
|
|
1123
|
-
titleField,
|
|
1124
|
-
pathTail,
|
|
1125
|
-
];
|
|
1126
|
-
let score = 0;
|
|
1127
|
-
for (const token of tokens) {
|
|
1128
|
-
if (titleField === token || pathTail === token) {
|
|
1129
|
-
score += 12;
|
|
1130
|
-
}
|
|
1131
|
-
else if (titleField.startsWith(token) || pathTail.startsWith(token)) {
|
|
1132
|
-
score += 8;
|
|
1133
|
-
}
|
|
1134
|
-
else if (titleField.includes(token) || pathTail.includes(token)) {
|
|
1135
|
-
score += 4;
|
|
1136
|
-
}
|
|
1137
|
-
score += fields.filter((field) => field.includes(token)).length;
|
|
1138
|
-
}
|
|
1139
|
-
return score;
|
|
1140
|
-
}
|
|
1141
|
-
getCandidateKey(candidate) {
|
|
1142
|
-
if (candidate.sourceKind === "git") {
|
|
1143
|
-
return `${candidate.sourceId}:${candidate.relativePath ?? "."}`;
|
|
1144
|
-
}
|
|
1145
|
-
return candidate.locator;
|
|
1146
|
-
}
|
|
1147
|
-
getCandidateTitle(leaf) {
|
|
1148
|
-
const title = leaf.title.trim();
|
|
1149
|
-
if (title.length === 0 || /^\{[^}]+\}$/.test(title)) {
|
|
1150
|
-
return leaf.linkName || leaf.name;
|
|
1151
|
-
}
|
|
1152
|
-
return title;
|
|
1153
|
-
}
|
|
1154
|
-
normalizeSearchQuery(value) {
|
|
1155
|
-
return value.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
|
|
1156
|
-
}
|
|
1157
|
-
formatSourceLabel(locator, displayName) {
|
|
1158
|
-
if (locator.startsWith("clawhub:")) {
|
|
1159
|
-
return `${displayName}@clawhub`;
|
|
1160
|
-
}
|
|
1161
|
-
if (path.isAbsolute(locator)) {
|
|
1162
|
-
return `${displayName}@local`;
|
|
1163
|
-
}
|
|
1164
|
-
const repo = parseGitHubRepo(locator);
|
|
1165
|
-
if (!repo) {
|
|
1166
|
-
return displayName;
|
|
1167
|
-
}
|
|
1168
|
-
return `${displayName}@${repo.owner}`;
|
|
1169
|
-
}
|
|
1170
|
-
applySourceUpdateResults(manifest, lockFile, updates) {
|
|
1171
|
-
for (const update of updates) {
|
|
1172
|
-
if (!update.changed) {
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
const source = manifest.sources.find((item) => item.id === update.sourceId);
|
|
1176
|
-
const binding = manifest.bindings[update.sourceId];
|
|
1177
|
-
if (!source || !binding) {
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
for (const diff of update.diffs) {
|
|
1181
|
-
if (diff.kind !== "moved" || !diff.previousLeafId) {
|
|
1182
|
-
continue;
|
|
1183
|
-
}
|
|
1184
|
-
for (const targetBinding of Object.values(binding.targets)) {
|
|
1185
|
-
if (!targetBinding?.enabled || !targetBinding.leafIds.includes(diff.previousLeafId)) {
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
targetBinding.leafIds = targetBinding.leafIds.map((leafId) => leafId === diff.previousLeafId ? diff.leafId : leafId);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
if ((update.selectionMode ?? source.selectionMode) !== "all") {
|
|
1192
|
-
continue;
|
|
1193
|
-
}
|
|
1194
|
-
const addedLeafIds = update.diffs
|
|
1195
|
-
.filter((diff) => diff.kind === "added")
|
|
1196
|
-
.map((diff) => diff.leafId);
|
|
1197
|
-
if (addedLeafIds.length === 0) {
|
|
1198
|
-
continue;
|
|
1199
|
-
}
|
|
1200
|
-
for (const targetBinding of Object.values(binding.targets)) {
|
|
1201
|
-
if (!targetBinding?.enabled) {
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
|
-
const merged = new Set([...targetBinding.leafIds, ...addedLeafIds]);
|
|
1205
|
-
targetBinding.leafIds = [...merged].filter((leafId) => lockFile.leafInventory.some((leaf) => leaf.id === leafId && leaf.sourceId === update.sourceId));
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
//# sourceMappingURL=skill-flow.js.map
|