skill-flow 1.0.8 → 1.3.1

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.
Files changed (143) hide show
  1. package/README.md +208 -131
  2. package/README.zh.md +169 -131
  3. package/dist/bridge-command.d.ts +9 -0
  4. package/dist/bridge-command.js +385 -0
  5. package/dist/bridge-command.js.map +1 -0
  6. package/dist/cli.js +68 -8
  7. package/dist/cli.js.map +1 -1
  8. package/package.json +11 -2
  9. package/dist/adapters/channel-adapters.d.ts +0 -8
  10. package/dist/adapters/channel-adapters.js +0 -64
  11. package/dist/adapters/channel-adapters.js.map +0 -1
  12. package/dist/domain/types.d.ts +0 -234
  13. package/dist/domain/types.js +0 -2
  14. package/dist/domain/types.js.map +0 -1
  15. package/dist/services/config-coordinator.d.ts +0 -38
  16. package/dist/services/config-coordinator.js +0 -83
  17. package/dist/services/config-coordinator.js.map +0 -1
  18. package/dist/services/deployment-applier.d.ts +0 -10
  19. package/dist/services/deployment-applier.js +0 -84
  20. package/dist/services/deployment-applier.js.map +0 -1
  21. package/dist/services/deployment-planner.d.ts +0 -16
  22. package/dist/services/deployment-planner.js +0 -366
  23. package/dist/services/deployment-planner.js.map +0 -1
  24. package/dist/services/doctor-service.d.ts +0 -7
  25. package/dist/services/doctor-service.js +0 -204
  26. package/dist/services/doctor-service.js.map +0 -1
  27. package/dist/services/inventory-service.d.ts +0 -17
  28. package/dist/services/inventory-service.js +0 -216
  29. package/dist/services/inventory-service.js.map +0 -1
  30. package/dist/services/skill-flow.d.ts +0 -136
  31. package/dist/services/skill-flow.js +0 -1210
  32. package/dist/services/skill-flow.js.map +0 -1
  33. package/dist/services/source-service.d.ts +0 -57
  34. package/dist/services/source-service.js +0 -809
  35. package/dist/services/source-service.js.map +0 -1
  36. package/dist/services/workflow-service.d.ts +0 -5
  37. package/dist/services/workflow-service.js +0 -45
  38. package/dist/services/workflow-service.js.map +0 -1
  39. package/dist/services/workspace-bootstrap-service.d.ts +0 -25
  40. package/dist/services/workspace-bootstrap-service.js +0 -140
  41. package/dist/services/workspace-bootstrap-service.js.map +0 -1
  42. package/dist/state/store.d.ts +0 -35
  43. package/dist/state/store.js +0 -151
  44. package/dist/state/store.js.map +0 -1
  45. package/dist/tests/add-flow-model.test.d.ts +0 -1
  46. package/dist/tests/add-flow-model.test.js +0 -108
  47. package/dist/tests/add-flow-model.test.js.map +0 -1
  48. package/dist/tests/add-flow-ui.test.d.ts +0 -1
  49. package/dist/tests/add-flow-ui.test.js +0 -16
  50. package/dist/tests/add-flow-ui.test.js.map +0 -1
  51. package/dist/tests/add-prepare-flow.test.d.ts +0 -1
  52. package/dist/tests/add-prepare-flow.test.js +0 -166
  53. package/dist/tests/add-prepare-flow.test.js.map +0 -1
  54. package/dist/tests/add-selection-and-find-command.test.d.ts +0 -1
  55. package/dist/tests/add-selection-and-find-command.test.js +0 -89
  56. package/dist/tests/add-selection-and-find-command.test.js.map +0 -1
  57. package/dist/tests/clawhub.test.d.ts +0 -1
  58. package/dist/tests/clawhub.test.js +0 -63
  59. package/dist/tests/clawhub.test.js.map +0 -1
  60. package/dist/tests/cli-utils.test.d.ts +0 -1
  61. package/dist/tests/cli-utils.test.js +0 -24
  62. package/dist/tests/cli-utils.test.js.map +0 -1
  63. package/dist/tests/config-coordinator.test.d.ts +0 -1
  64. package/dist/tests/config-coordinator.test.js +0 -219
  65. package/dist/tests/config-coordinator.test.js.map +0 -1
  66. package/dist/tests/config-integration.test.d.ts +0 -1
  67. package/dist/tests/config-integration.test.js +0 -276
  68. package/dist/tests/config-integration.test.js.map +0 -1
  69. package/dist/tests/config-ui-utils.test.d.ts +0 -1
  70. package/dist/tests/config-ui-utils.test.js +0 -523
  71. package/dist/tests/config-ui-utils.test.js.map +0 -1
  72. package/dist/tests/find-and-naming-utils.test.d.ts +0 -1
  73. package/dist/tests/find-and-naming-utils.test.js +0 -127
  74. package/dist/tests/find-and-naming-utils.test.js.map +0 -1
  75. package/dist/tests/inventory-service-precedence.test.d.ts +0 -1
  76. package/dist/tests/inventory-service-precedence.test.js +0 -42
  77. package/dist/tests/inventory-service-precedence.test.js.map +0 -1
  78. package/dist/tests/skill-flow.test.d.ts +0 -1
  79. package/dist/tests/skill-flow.test.js +0 -991
  80. package/dist/tests/skill-flow.test.js.map +0 -1
  81. package/dist/tests/source-lifecycle.test.d.ts +0 -1
  82. package/dist/tests/source-lifecycle.test.js +0 -644
  83. package/dist/tests/source-lifecycle.test.js.map +0 -1
  84. package/dist/tests/source-parsing-compatibility.test.d.ts +0 -1
  85. package/dist/tests/source-parsing-compatibility.test.js +0 -72
  86. package/dist/tests/source-parsing-compatibility.test.js.map +0 -1
  87. package/dist/tests/target-definitions.test.d.ts +0 -1
  88. package/dist/tests/target-definitions.test.js +0 -51
  89. package/dist/tests/target-definitions.test.js.map +0 -1
  90. package/dist/tests/test-helpers.d.ts +0 -18
  91. package/dist/tests/test-helpers.js +0 -123
  92. package/dist/tests/test-helpers.js.map +0 -1
  93. package/dist/tui/add-flow-model.d.ts +0 -62
  94. package/dist/tui/add-flow-model.js +0 -206
  95. package/dist/tui/add-flow-model.js.map +0 -1
  96. package/dist/tui/add-flow.d.ts +0 -25
  97. package/dist/tui/add-flow.js +0 -534
  98. package/dist/tui/add-flow.js.map +0 -1
  99. package/dist/tui/config-app.d.ts +0 -178
  100. package/dist/tui/config-app.js +0 -1551
  101. package/dist/tui/config-app.js.map +0 -1
  102. package/dist/tui/find-app.d.ts +0 -9
  103. package/dist/tui/find-app.js +0 -150
  104. package/dist/tui/find-app.js.map +0 -1
  105. package/dist/tui/selection-state.d.ts +0 -8
  106. package/dist/tui/selection-state.js +0 -32
  107. package/dist/tui/selection-state.js.map +0 -1
  108. package/dist/utils/builtin-git-sources.d.ts +0 -5
  109. package/dist/utils/builtin-git-sources.js +0 -23
  110. package/dist/utils/builtin-git-sources.js.map +0 -1
  111. package/dist/utils/clawhub.d.ts +0 -41
  112. package/dist/utils/clawhub.js +0 -94
  113. package/dist/utils/clawhub.js.map +0 -1
  114. package/dist/utils/cli.d.ts +0 -2
  115. package/dist/utils/cli.js +0 -19
  116. package/dist/utils/cli.js.map +0 -1
  117. package/dist/utils/constants.d.ts +0 -23
  118. package/dist/utils/constants.js +0 -195
  119. package/dist/utils/constants.js.map +0 -1
  120. package/dist/utils/find-command.d.ts +0 -2
  121. package/dist/utils/find-command.js +0 -29
  122. package/dist/utils/find-command.js.map +0 -1
  123. package/dist/utils/format.d.ts +0 -7
  124. package/dist/utils/format.js +0 -68
  125. package/dist/utils/format.js.map +0 -1
  126. package/dist/utils/fs.d.ts +0 -16
  127. package/dist/utils/fs.js +0 -144
  128. package/dist/utils/fs.js.map +0 -1
  129. package/dist/utils/git.d.ts +0 -3
  130. package/dist/utils/git.js +0 -12
  131. package/dist/utils/git.js.map +0 -1
  132. package/dist/utils/github-catalog.d.ts +0 -1
  133. package/dist/utils/github-catalog.js +0 -25
  134. package/dist/utils/github-catalog.js.map +0 -1
  135. package/dist/utils/naming.d.ts +0 -29
  136. package/dist/utils/naming.js +0 -115
  137. package/dist/utils/naming.js.map +0 -1
  138. package/dist/utils/result.d.ts +0 -4
  139. package/dist/utils/result.js +0 -15
  140. package/dist/utils/result.js.map +0 -1
  141. package/dist/utils/source-id.d.ts +0 -2
  142. package/dist/utils/source-id.js +0 -49
  143. 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