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.
Files changed (143) hide show
  1. package/README.md +209 -131
  2. package/README.zh.md +170 -131
  3. package/dist/bridge-command.d.ts +9 -0
  4. package/dist/bridge-command.js +422 -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,809 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { copyDirectory, ensureDir, hashDirectory, isPathInside, pathExists, readJsonFile, removePath, } from "../utils/fs.js";
5
- import { installClawHubSkill, } from "../utils/clawhub.js";
6
- import { git } from "../utils/git.js";
7
- import { fail, ok } from "../utils/result.js";
8
- import { deriveDisplayName, deriveSourceId } from "../utils/source-id.js";
9
- import { formatGroupLabel } from "../utils/naming.js";
10
- export class SourceService {
11
- store;
12
- inventoryService;
13
- constructor(store, inventoryService) {
14
- this.store = store;
15
- this.inventoryService = inventoryService;
16
- }
17
- async addSource(locator, options = {}) {
18
- const { manifest, lockFile } = await this.store.readState();
19
- const resolved = this.resolveUniqueLocalSource(await this.resolveSource(locator, options), manifest.sources);
20
- if (manifest.sources.some((source) => source.id === resolved.sourceId && source.locator === resolved.locator)) {
21
- return fail({
22
- code: "SOURCE_EXISTS",
23
- message: `Skills group '${formatGroupLabel({ id: resolved.sourceId, locator: resolved.locator, displayName: resolved.displayName })}' is already registered with id '${resolved.sourceId}'.`,
24
- });
25
- }
26
- if (manifest.sources.some((source) => source.id === resolved.sourceId)) {
27
- return fail({
28
- code: "SOURCE_EXISTS",
29
- message: `Skills group '${formatGroupLabel({ id: resolved.sourceId, locator: resolved.locator, displayName: resolved.displayName })}' is already registered with id '${resolved.sourceId}'.`,
30
- });
31
- }
32
- const checkoutPath = this.store.getSourceCheckoutPath(resolved.kind, resolved.sourceId);
33
- const tempCheckoutPath = `${checkoutPath}.${process.pid}.${crypto.randomUUID()}.add`;
34
- await ensureDir(this.store.getSourceRoot(resolved.kind));
35
- try {
36
- await this.fetchSource(resolved, tempCheckoutPath);
37
- }
38
- catch (error) {
39
- await removePath(tempCheckoutPath);
40
- return fail({
41
- code: resolved.kind === "git"
42
- ? "GIT_CLONE_FAILED"
43
- : resolved.kind === "local"
44
- ? "LOCAL_IMPORT_FAILED"
45
- : "CLAWHUB_FETCH_FAILED",
46
- message: `Unable to fetch source '${resolved.locator}': ${String(error)}`,
47
- });
48
- }
49
- const snapshot = await this.buildSnapshot(resolved.kind, resolved.sourceId, resolved.locator, resolved.displayName, tempCheckoutPath, resolved.requestedPath, options);
50
- if (!snapshot.ok) {
51
- await removePath(tempCheckoutPath);
52
- return fail(snapshot.errors, snapshot.warnings);
53
- }
54
- if (await pathExists(checkoutPath)) {
55
- await removePath(tempCheckoutPath);
56
- return fail({
57
- code: "SOURCE_CHECKOUT_PATH_EXISTS",
58
- message: `Unable to register source '${resolved.locator}' because checkout path already exists at ${checkoutPath}.`,
59
- });
60
- }
61
- try {
62
- await fs.rename(tempCheckoutPath, checkoutPath);
63
- }
64
- catch (error) {
65
- await removePath(tempCheckoutPath).catch(() => { });
66
- return fail({
67
- code: "SOURCE_CHECKOUT_MOVE_FAILED",
68
- message: `Unable to finalize source '${resolved.locator}' at ${checkoutPath}: ${String(error)}`,
69
- });
70
- }
71
- snapshot.data.lock.checkoutPath = checkoutPath;
72
- snapshot.data.leafs = snapshot.data.leafs.map((leaf) => ({
73
- ...leaf,
74
- absolutePath: path.join(checkoutPath, leaf.relativePath),
75
- skillFilePath: path.join(checkoutPath, leaf.relativePath, "SKILL.md"),
76
- }));
77
- manifest.sources.push(snapshot.data.manifest);
78
- manifest.bindings[resolved.sourceId] = { targets: {} };
79
- lockFile.sources.push(snapshot.data.lock);
80
- lockFile.leafInventory.push(...snapshot.data.leafs);
81
- await this.store.writeState(manifest, lockFile);
82
- return ok({
83
- manifest: snapshot.data.manifest,
84
- lock: snapshot.data.lock,
85
- leafCount: snapshot.data.leafs.length,
86
- invalidLeafCount: snapshot.data.lock.invalidLeafs.length,
87
- }, snapshot.warnings);
88
- }
89
- async updateSources(sourceIds) {
90
- const { manifest, lockFile } = await this.store.readState();
91
- const selectedIds = sourceIds?.length
92
- ? sourceIds
93
- : manifest.sources.map((source) => source.id);
94
- const updated = [];
95
- for (const sourceId of selectedIds) {
96
- const source = manifest.sources.find((item) => item.id === sourceId);
97
- const currentLock = lockFile.sources.find((item) => item.id === sourceId);
98
- if (!source || !currentLock) {
99
- return fail({
100
- code: "SOURCE_NOT_FOUND",
101
- message: `Skills group id '${sourceId}' is not registered.`,
102
- });
103
- }
104
- try {
105
- const sourceChanged = await this.updateSource(source, currentLock);
106
- const sourceMeta = {
107
- ...(source.requestedPath ? { requestedPath: source.requestedPath } : {}),
108
- ...(source.selectionMode ? { selectionMode: source.selectionMode } : {}),
109
- };
110
- if (!sourceChanged) {
111
- updated.push({
112
- sourceId,
113
- changed: false,
114
- addedLeafIds: [],
115
- removedLeafIds: [],
116
- invalidatedLeafIds: [],
117
- diffs: [],
118
- ...sourceMeta,
119
- });
120
- continue;
121
- }
122
- const snapshot = await this.buildSnapshot(source.kind, source.id, source.locator, source.displayName, currentLock.checkoutPath, source.requestedPath, {}, { allowEmptyLeafs: true });
123
- if (!snapshot.ok) {
124
- return fail(snapshot.errors, snapshot.warnings);
125
- }
126
- const diff = this.buildSourceUpdateDiff(source, lockFile.leafInventory.filter((leaf) => leaf.sourceId === sourceId), snapshot.data.leafs, snapshot.data.lock.invalidLeafs);
127
- lockFile.sources = lockFile.sources.map((item) => item.id === sourceId ? snapshot.data.lock : item);
128
- lockFile.leafInventory = [
129
- ...lockFile.leafInventory.filter((leaf) => leaf.sourceId !== sourceId),
130
- ...snapshot.data.leafs,
131
- ];
132
- updated.push({
133
- sourceId,
134
- changed: sourceChanged,
135
- addedLeafIds: diff.addedLeafIds,
136
- removedLeafIds: diff.removedLeafIds,
137
- invalidatedLeafIds: diff.invalidatedLeafIds,
138
- diffs: diff.diffs,
139
- ...sourceMeta,
140
- });
141
- }
142
- catch (error) {
143
- return fail({
144
- code: source.kind === "git"
145
- ? "GIT_UPDATE_FAILED"
146
- : source.kind === "local"
147
- ? "LOCAL_UPDATE_FAILED"
148
- : "CLAWHUB_UPDATE_FAILED",
149
- message: `Unable to update skills group id '${sourceId}': ${String(error)}`,
150
- });
151
- }
152
- }
153
- await this.store.writeState(manifest, lockFile);
154
- return ok({ updated });
155
- }
156
- async removeSource(sourceIds) {
157
- const { manifest, lockFile } = await this.store.readState();
158
- const removed = [];
159
- for (const sourceId of sourceIds) {
160
- const currentSource = manifest.sources.find((source) => source.id === sourceId);
161
- const currentLock = lockFile.sources.find((source) => source.id === sourceId);
162
- if (!currentSource || !currentLock) {
163
- return fail({
164
- code: "SOURCE_NOT_FOUND",
165
- message: `Skills group id '${sourceId}' is not registered.`,
166
- });
167
- }
168
- manifest.sources = manifest.sources.filter((source) => source.id !== sourceId);
169
- delete manifest.bindings[sourceId];
170
- lockFile.sources = lockFile.sources.filter((source) => source.id !== sourceId);
171
- lockFile.leafInventory = lockFile.leafInventory.filter((leaf) => leaf.sourceId !== sourceId);
172
- lockFile.deployments = lockFile.deployments.filter((deployment) => deployment.sourceId !== sourceId);
173
- const checkoutRoot = this.store.getSourceRoot(currentSource.kind);
174
- if (!isPathInside(checkoutRoot, currentLock.checkoutPath)) {
175
- return fail({
176
- code: "SOURCE_CHECKOUT_PATH_INVALID",
177
- message: `Refusing to delete checkout outside managed root: ${currentLock.checkoutPath}`,
178
- });
179
- }
180
- if (currentLock && (await pathExists(currentLock.checkoutPath))) {
181
- await removePath(currentLock.checkoutPath);
182
- }
183
- removed.push(sourceId);
184
- }
185
- await this.store.writeState(manifest, lockFile);
186
- return ok({ removed });
187
- }
188
- async reconcileInventory(sourceIds, options = {}) {
189
- const { manifest, lockFile } = await this.store.readState();
190
- const selectedIds = sourceIds?.length
191
- ? sourceIds
192
- : manifest.sources.map((source) => source.id);
193
- const updatedSourceIds = [];
194
- for (const sourceId of selectedIds) {
195
- const source = manifest.sources.find((item) => item.id === sourceId);
196
- const currentLock = lockFile.sources.find((item) => item.id === sourceId);
197
- if (!source || !currentLock) {
198
- continue;
199
- }
200
- const sourceLeafs = lockFile.leafInventory.filter((leaf) => leaf.sourceId === sourceId);
201
- const sourceDeployments = lockFile.deployments.filter((deployment) => deployment.sourceId === sourceId);
202
- if (!options.force &&
203
- !this.needsInventoryReconcile(sourceId, sourceLeafs, sourceDeployments)) {
204
- continue;
205
- }
206
- const snapshot = await this.buildSnapshot(source.kind, source.id, source.locator, source.displayName, currentLock.checkoutPath, source.requestedPath, {}, { allowEmptyLeafs: true });
207
- if (!snapshot.ok) {
208
- return fail(snapshot.errors, snapshot.warnings);
209
- }
210
- const leafIdsChanged = JSON.stringify(currentLock.leafIds) !==
211
- JSON.stringify(snapshot.data.lock.leafIds);
212
- const invalidLeafsChanged = JSON.stringify(currentLock.invalidLeafs) !==
213
- JSON.stringify(snapshot.data.lock.invalidLeafs);
214
- const leafInventoryChanged = JSON.stringify(sourceLeafs) !== JSON.stringify(snapshot.data.leafs);
215
- if (!options.force &&
216
- !leafIdsChanged &&
217
- !invalidLeafsChanged &&
218
- !leafInventoryChanged) {
219
- continue;
220
- }
221
- lockFile.sources = lockFile.sources.map((item) => item.id === sourceId ? snapshot.data.lock : item);
222
- lockFile.leafInventory = [
223
- ...lockFile.leafInventory.filter((leaf) => leaf.sourceId !== sourceId),
224
- ...snapshot.data.leafs,
225
- ];
226
- const nextLeafIds = new Set(snapshot.data.leafs.map((leaf) => leaf.id));
227
- const binding = manifest.bindings[sourceId];
228
- if (binding) {
229
- for (const targetBinding of Object.values(binding.targets)) {
230
- if (!targetBinding) {
231
- continue;
232
- }
233
- targetBinding.leafIds = targetBinding.leafIds.filter((leafId) => nextLeafIds.has(leafId));
234
- }
235
- }
236
- updatedSourceIds.push(sourceId);
237
- }
238
- if (updatedSourceIds.length > 0) {
239
- await this.store.writeState(manifest, lockFile);
240
- }
241
- return ok({ updatedSourceIds });
242
- }
243
- needsInventoryReconcile(sourceId, sourceLeafs, sourceDeployments) {
244
- const hasGeneratedLeafs = sourceLeafs.some((leaf) => /^(?:\.agents|\.claude|\.codex|\.opencode|\.openclaw)(?:\/|$)/.test(leaf.relativePath));
245
- const hasLegacyTargetNames = sourceDeployments.some((deployment) => path.basename(deployment.targetPath).startsWith(`${sourceId}--`));
246
- return hasGeneratedLeafs || hasLegacyTargetNames;
247
- }
248
- buildSourceUpdateDiff(source, previousLeafs, nextLeafs, nextInvalidLeafs) {
249
- const requestedPath = this.normalizeRequestedPath(source.requestedPath);
250
- const requestedPathOption = requestedPath ? { requestedPath } : {};
251
- const diffs = [];
252
- const addedLeafIds = [];
253
- const removedLeafIds = [];
254
- const invalidatedLeafIds = [];
255
- const matchedPreviousIds = new Set();
256
- const matchedNextIds = new Set();
257
- const nextById = new Map(nextLeafs.map((leaf) => [leaf.id, leaf]));
258
- const nextInvalidPaths = new Set(nextInvalidLeafs.map((leaf) => leaf.path));
259
- for (const previousLeaf of previousLeafs) {
260
- const nextLeaf = nextById.get(previousLeaf.id);
261
- if (!nextLeaf || previousLeaf.contentHash === nextLeaf.contentHash) {
262
- continue;
263
- }
264
- diffs.push(this.createDiffItem("changed", source.id, nextLeaf, {
265
- ...requestedPathOption,
266
- previousLeafId: previousLeaf.id,
267
- previousRelativePath: previousLeaf.relativePath,
268
- previousContentHash: previousLeaf.contentHash,
269
- }));
270
- matchedPreviousIds.add(previousLeaf.id);
271
- matchedNextIds.add(nextLeaf.id);
272
- }
273
- const previousByHash = new Map();
274
- const nextByHash = new Map();
275
- for (const previousLeaf of previousLeafs) {
276
- if (matchedPreviousIds.has(previousLeaf.id)) {
277
- continue;
278
- }
279
- const list = previousByHash.get(previousLeaf.contentHash) ?? [];
280
- list.push(previousLeaf);
281
- previousByHash.set(previousLeaf.contentHash, list);
282
- }
283
- for (const nextLeaf of nextLeafs) {
284
- if (matchedNextIds.has(nextLeaf.id)) {
285
- continue;
286
- }
287
- const list = nextByHash.get(nextLeaf.contentHash) ?? [];
288
- list.push(nextLeaf);
289
- nextByHash.set(nextLeaf.contentHash, list);
290
- }
291
- for (const contentHash of [...previousByHash.keys()].sort()) {
292
- const previousGroup = previousByHash.get(contentHash) ?? [];
293
- const nextGroup = nextByHash.get(contentHash) ?? [];
294
- if (previousGroup.length !== 1 || nextGroup.length !== 1) {
295
- continue;
296
- }
297
- const previousLeaf = previousGroup[0];
298
- const nextLeaf = nextGroup[0];
299
- if (previousLeaf.id === nextLeaf.id ||
300
- !this.canClassifyAsMoved(requestedPath, previousLeaf.relativePath, nextLeaf.relativePath)) {
301
- continue;
302
- }
303
- diffs.push(this.createDiffItem("moved", source.id, nextLeaf, {
304
- ...requestedPathOption,
305
- previousLeafId: previousLeaf.id,
306
- previousRelativePath: previousLeaf.relativePath,
307
- previousContentHash: previousLeaf.contentHash,
308
- }));
309
- matchedPreviousIds.add(previousLeaf.id);
310
- matchedNextIds.add(nextLeaf.id);
311
- }
312
- for (const previousLeaf of previousLeafs) {
313
- if (matchedPreviousIds.has(previousLeaf.id)) {
314
- continue;
315
- }
316
- if (nextInvalidPaths.has(previousLeaf.relativePath)) {
317
- diffs.push(this.createDiffItem("invalidated", source.id, previousLeaf, {
318
- ...requestedPathOption,
319
- previousLeafId: previousLeaf.id,
320
- previousRelativePath: previousLeaf.relativePath,
321
- previousContentHash: previousLeaf.contentHash,
322
- }));
323
- invalidatedLeafIds.push(previousLeaf.id);
324
- matchedPreviousIds.add(previousLeaf.id);
325
- }
326
- }
327
- for (const previousLeaf of previousLeafs) {
328
- if (matchedPreviousIds.has(previousLeaf.id)) {
329
- continue;
330
- }
331
- diffs.push(this.createDiffItem("removed", source.id, previousLeaf, {
332
- ...requestedPathOption,
333
- previousLeafId: previousLeaf.id,
334
- previousRelativePath: previousLeaf.relativePath,
335
- previousContentHash: previousLeaf.contentHash,
336
- }));
337
- removedLeafIds.push(previousLeaf.id);
338
- matchedPreviousIds.add(previousLeaf.id);
339
- }
340
- for (const nextLeaf of nextLeafs) {
341
- if (matchedNextIds.has(nextLeaf.id)) {
342
- continue;
343
- }
344
- diffs.push(this.createDiffItem("added", source.id, nextLeaf, requestedPathOption));
345
- addedLeafIds.push(nextLeaf.id);
346
- matchedNextIds.add(nextLeaf.id);
347
- }
348
- return {
349
- diffs,
350
- addedLeafIds,
351
- removedLeafIds,
352
- invalidatedLeafIds,
353
- };
354
- }
355
- createDiffItem(kind, sourceId, leaf, extras = {}) {
356
- return {
357
- kind,
358
- sourceId,
359
- leafId: leaf.id,
360
- relativePath: leaf.relativePath,
361
- contentHash: leaf.contentHash,
362
- ...(extras.requestedPath ? { requestedPath: extras.requestedPath } : {}),
363
- ...(extras.previousLeafId ? { previousLeafId: extras.previousLeafId } : {}),
364
- ...(extras.previousRelativePath ? { previousRelativePath: extras.previousRelativePath } : {}),
365
- ...(extras.previousContentHash ? { previousContentHash: extras.previousContentHash } : {}),
366
- };
367
- }
368
- canClassifyAsMoved(requestedPath, previousRelativePath, nextRelativePath) {
369
- if (!requestedPath) {
370
- return true;
371
- }
372
- return (this.isWithinRequestedPath(previousRelativePath, requestedPath) &&
373
- this.isWithinRequestedPath(nextRelativePath, requestedPath));
374
- }
375
- normalizeRequestedPath(requestedPath) {
376
- if (!requestedPath) {
377
- return undefined;
378
- }
379
- const normalized = requestedPath.replace(/^\.\/+/, "").replace(/\/+$/, "");
380
- return normalized.length > 0 ? normalized : undefined;
381
- }
382
- isWithinRequestedPath(relativePath, requestedPath) {
383
- const normalizedPath = this.normalizeRequestedPath(requestedPath);
384
- if (!normalizedPath) {
385
- return true;
386
- }
387
- return (relativePath === normalizedPath || relativePath.startsWith(`${normalizedPath}/`));
388
- }
389
- async buildSnapshot(kind, sourceId, locator, displayName, checkoutPath, requestedPathOrOptions = undefined, addOptions = {}, maybeOptions = {}) {
390
- const requestedPath = typeof requestedPathOrOptions === "string" ? requestedPathOrOptions : undefined;
391
- const options = typeof requestedPathOrOptions === "string"
392
- ? maybeOptions
393
- : (maybeOptions.allowEmptyLeafs !== undefined
394
- ? maybeOptions
395
- : (requestedPathOrOptions ?? {}));
396
- const sourceMetadata = await this.readSourceSnapshot(kind, checkoutPath);
397
- const scanned = await this.inventoryService.scanSource(sourceId, checkoutPath, displayName);
398
- const requestedMatches = this.findRequestedLeafs(scanned.leafs, requestedPath);
399
- const metadataWarnings = scanned.leafs.flatMap((leaf) => leaf.metadataWarnings.map((message) => ({
400
- code: "SKILL_METADATA_WARNING",
401
- message: `${leaf.relativePath}: ${message}`,
402
- })));
403
- if (((requestedPath && requestedMatches.length === 0) || scanned.leafs.length === 0) &&
404
- !options.allowEmptyLeafs) {
405
- const emptyReason = scanned.skillFileCount === 0
406
- ? " No SKILL.md files were found."
407
- : "";
408
- return fail({
409
- code: requestedPath ? "SOURCE_PATH_NOT_FOUND" : "NO_VALID_LEAFS",
410
- message: requestedPath
411
- ? `Source '${displayName}' does not contain a valid skill at '${requestedPath}'.`
412
- : `Source '${displayName}' has no valid skills.${emptyReason}`,
413
- }, scanned.invalidLeafs.map((leaf) => ({
414
- code: "INVALID_LEAF",
415
- message: `${leaf.path}: ${leaf.reason}`,
416
- })));
417
- }
418
- return ok({
419
- manifest: {
420
- id: sourceId,
421
- locator,
422
- kind,
423
- displayName,
424
- addedAt: new Date().toISOString(),
425
- ...(requestedPath ? { requestedPath } : {}),
426
- ...(addOptions.selectionMode ? { selectionMode: addOptions.selectionMode } : {}),
427
- ...(addOptions.originLocator ? { originLocator: addOptions.originLocator } : {}),
428
- ...(addOptions.originRequestedPath
429
- ? { originRequestedPath: addOptions.originRequestedPath }
430
- : {}),
431
- },
432
- lock: {
433
- id: sourceId,
434
- locator,
435
- kind,
436
- displayName,
437
- checkoutPath,
438
- updatedAt: new Date().toISOString(),
439
- leafIds: scanned.leafs.map((leaf) => leaf.id),
440
- invalidLeafs: scanned.invalidLeafs,
441
- ...sourceMetadata,
442
- ...(addOptions.originBranch ? { originBranch: addOptions.originBranch } : {}),
443
- ...(addOptions.importedFromTargets
444
- ? { importedFromTargets: addOptions.importedFromTargets }
445
- : {}),
446
- ...(addOptions.importMode ? { importMode: addOptions.importMode } : {}),
447
- ...(kind === "clawhub"
448
- ? {
449
- versionMode: locator.includes("@") ? "pinned" : "floating",
450
- }
451
- : {}),
452
- },
453
- leafs: scanned.leafs,
454
- }, [
455
- ...metadataWarnings,
456
- ...scanned.duplicateLeafs.map((leaf) => ({
457
- code: "DUPLICATE_LEAF",
458
- message: `${leaf.path}: Duplicate skill content skipped because ${leaf.keptPath} was discovered first`,
459
- })),
460
- ...scanned.invalidLeafs.map((leaf) => ({
461
- code: "INVALID_LEAF",
462
- message: `${leaf.path}: ${leaf.reason}`,
463
- })),
464
- ]);
465
- }
466
- async normalizeLocator(locator) {
467
- const trimmed = locator.trim();
468
- if (trimmed.startsWith("git@") || trimmed.startsWith("http")) {
469
- return trimmed;
470
- }
471
- if (/^[^/\s]+\/[^/\s]+$/.test(trimmed)) {
472
- return `https://github.com/${trimmed}.git`;
473
- }
474
- const resolvedPath = path.resolve(trimmed);
475
- if (await pathExists(resolvedPath)) {
476
- return resolvedPath;
477
- }
478
- return trimmed;
479
- }
480
- async resolveSource(locator, options) {
481
- const trimmed = locator.trim();
482
- const fileLocatorPath = this.parseFileLocator(trimmed);
483
- const resolvedPath = path.resolve(fileLocatorPath ?? trimmed);
484
- if (await pathExists(resolvedPath) &&
485
- (!fileLocatorPath || !(await this.isGitRepositoryPath(resolvedPath)))) {
486
- return {
487
- kind: "local",
488
- locator: resolvedPath,
489
- localPath: resolvedPath,
490
- displayName: options.displayNameOverride ?? deriveDisplayName(resolvedPath),
491
- sourceId: options.sourceIdOverride ?? deriveSourceId(resolvedPath),
492
- ...(options.path ? { requestedPath: options.path } : {}),
493
- };
494
- }
495
- const treeLocator = this.parseTreeLocator(trimmed);
496
- if (treeLocator) {
497
- const requestedPath = this.joinRequestedPaths(treeLocator.requestedPath, options.path);
498
- return {
499
- kind: "git",
500
- locator: treeLocator.repoLocator,
501
- gitLocator: await this.normalizeLocator(treeLocator.repoLocator),
502
- displayName: options.displayNameOverride ?? deriveDisplayName(treeLocator.repoLocator),
503
- sourceId: options.sourceIdOverride ?? deriveSourceId(treeLocator.repoLocator),
504
- ...(requestedPath ? { requestedPath } : {}),
505
- };
506
- }
507
- const shorthandLocator = this.parseGitHubShorthandSubpath(trimmed);
508
- if (shorthandLocator) {
509
- const requestedPath = this.joinRequestedPaths(shorthandLocator.requestedPath, options.path);
510
- return {
511
- kind: "git",
512
- locator: shorthandLocator.repoLocator,
513
- gitLocator: await this.normalizeLocator(shorthandLocator.repoLocator),
514
- displayName: options.displayNameOverride ?? deriveDisplayName(shorthandLocator.repoLocator),
515
- sourceId: options.sourceIdOverride ?? deriveSourceId(shorthandLocator.repoLocator),
516
- ...(requestedPath ? { requestedPath } : {}),
517
- };
518
- }
519
- const clawhubMatch = trimmed.match(/^clawhub:([^@\s]+)(?:@(.+))?$/);
520
- if (clawhubMatch) {
521
- const slug = clawhubMatch[1];
522
- const version = clawhubMatch[2];
523
- if (!slug) {
524
- throw new Error(`Invalid ClawHub locator '${locator}'.`);
525
- }
526
- return version
527
- ? {
528
- kind: "clawhub",
529
- locator: trimmed,
530
- displayName: options.displayNameOverride ?? deriveDisplayName(trimmed),
531
- sourceId: options.sourceIdOverride ?? deriveSourceId(trimmed),
532
- ...(options.path ? { requestedPath: options.path } : {}),
533
- clawhubSlug: slug,
534
- requestedVersion: version,
535
- versionMode: "pinned",
536
- }
537
- : {
538
- kind: "clawhub",
539
- locator: trimmed,
540
- displayName: options.displayNameOverride ?? deriveDisplayName(trimmed),
541
- sourceId: options.sourceIdOverride ?? deriveSourceId(trimmed),
542
- ...(options.path ? { requestedPath: options.path } : {}),
543
- clawhubSlug: slug,
544
- versionMode: "floating",
545
- };
546
- }
547
- return {
548
- kind: "git",
549
- locator,
550
- gitLocator: await this.normalizeLocator(locator),
551
- displayName: options.displayNameOverride ?? deriveDisplayName(locator),
552
- sourceId: options.sourceIdOverride ?? deriveSourceId(locator),
553
- ...(options.path ? { requestedPath: options.path } : {}),
554
- };
555
- }
556
- resolveUniqueLocalSource(resolved, existingSources) {
557
- if (resolved.kind !== "local" || !resolved.localPath) {
558
- return resolved;
559
- }
560
- if (existingSources.some((source) => source.kind === "local" && path.resolve(source.locator) === resolved.localPath)) {
561
- return resolved;
562
- }
563
- const folderName = path.basename(resolved.localPath);
564
- const parentFolderName = path.basename(path.dirname(resolved.localPath));
565
- const displayCandidates = [
566
- folderName,
567
- `${parentFolderName}_${folderName}`,
568
- ];
569
- const takenIds = new Set(existingSources.map((source) => source.id));
570
- const takenLabels = new Set(existingSources
571
- .filter((source) => source.kind === "local")
572
- .map((source) => source.displayName));
573
- for (const candidate of displayCandidates) {
574
- const sourceId = deriveSourceId(candidate);
575
- if (!takenIds.has(sourceId) && !takenLabels.has(candidate)) {
576
- return {
577
- ...resolved,
578
- displayName: candidate,
579
- sourceId,
580
- };
581
- }
582
- }
583
- const baseDisplayName = `${parentFolderName}_${folderName}`;
584
- let index = 2;
585
- while (true) {
586
- const displayName = `${baseDisplayName}_${index}`;
587
- const sourceId = deriveSourceId(displayName);
588
- if (!takenIds.has(sourceId) && !takenLabels.has(displayName)) {
589
- return {
590
- ...resolved,
591
- displayName,
592
- sourceId,
593
- };
594
- }
595
- index += 1;
596
- }
597
- }
598
- parseTreeLocator(locator) {
599
- try {
600
- const url = new URL(locator);
601
- const parts = url.pathname.split("/").filter(Boolean);
602
- if (url.hostname === "github.com") {
603
- if (parts.length < 5 || parts[2] !== "tree") {
604
- return null;
605
- }
606
- const owner = parts[0];
607
- const repo = parts[1];
608
- const requestedPath = parts.slice(4).join("/");
609
- if (!owner || !repo || !requestedPath) {
610
- return null;
611
- }
612
- return {
613
- repoLocator: `https://github.com/${owner}/${repo}.git`,
614
- requestedPath,
615
- };
616
- }
617
- if (url.hostname === "gitlab.com") {
618
- const markerIndex = parts.findIndex((segment, index) => segment === "-" && parts[index + 1] === "tree");
619
- if (markerIndex < 2) {
620
- return null;
621
- }
622
- const requestedPath = parts.slice(markerIndex + 3).join("/");
623
- return {
624
- repoLocator: `https://gitlab.com/${parts.slice(0, markerIndex).join("/")}.git`,
625
- ...(requestedPath ? { requestedPath } : {}),
626
- };
627
- }
628
- }
629
- catch {
630
- return null;
631
- }
632
- return null;
633
- }
634
- parseGitHubShorthandSubpath(locator) {
635
- const trimmed = locator.replace(/\/+$/, "");
636
- const parts = trimmed.split("/");
637
- if (parts.length < 3) {
638
- return null;
639
- }
640
- const owner = parts[0];
641
- const rawRepo = parts[1];
642
- const requestedPath = parts.slice(2).join("/");
643
- if (!owner || !rawRepo || !requestedPath) {
644
- return null;
645
- }
646
- const repo = rawRepo.replace(/\.git$/i, "");
647
- return {
648
- repoLocator: `https://github.com/${owner}/${repo}.git`,
649
- requestedPath,
650
- };
651
- }
652
- parseFileLocator(locator) {
653
- if (!locator.startsWith("file://")) {
654
- return null;
655
- }
656
- try {
657
- const fileUrl = new URL(locator);
658
- if (fileUrl.protocol !== "file:") {
659
- return null;
660
- }
661
- return path.resolve(decodeURIComponent(fileUrl.pathname));
662
- }
663
- catch {
664
- return null;
665
- }
666
- }
667
- async isGitRepositoryPath(candidatePath) {
668
- if (await pathExists(path.join(candidatePath, ".git"))) {
669
- return true;
670
- }
671
- return (await pathExists(path.join(candidatePath, "HEAD")) &&
672
- await pathExists(path.join(candidatePath, "objects")) &&
673
- await pathExists(path.join(candidatePath, "refs")));
674
- }
675
- joinRequestedPaths(basePath, childPath) {
676
- const normalizedBase = this.normalizeRequestedPath(basePath);
677
- const normalizedChild = this.normalizeRequestedPath(childPath);
678
- if (!normalizedBase) {
679
- return normalizedChild;
680
- }
681
- if (!normalizedChild) {
682
- return normalizedBase;
683
- }
684
- if (normalizedChild === normalizedBase ||
685
- normalizedChild.startsWith(`${normalizedBase}/`)) {
686
- return normalizedChild;
687
- }
688
- return `${normalizedBase}/${normalizedChild}`;
689
- }
690
- findRequestedLeafs(leafs, requestedPath) {
691
- const normalizedPath = this.normalizeRequestedPath(requestedPath);
692
- if (!normalizedPath) {
693
- return leafs;
694
- }
695
- return leafs.filter((leaf) => leaf.relativePath === normalizedPath ||
696
- leaf.relativePath.startsWith(`${normalizedPath}/`));
697
- }
698
- async fetchSource(source, checkoutPath) {
699
- if (source.kind === "local") {
700
- await copyDirectory(source.localPath, checkoutPath);
701
- return;
702
- }
703
- if (source.kind === "git") {
704
- await git(["clone", "--depth", "1", source.gitLocator, checkoutPath]);
705
- return;
706
- }
707
- if (source.kind === "clawhub") {
708
- const installed = await installClawHubSkill(source.clawhubSlug, source.requestedVersion);
709
- try {
710
- await copyDirectory(installed.installedPath, checkoutPath);
711
- }
712
- finally {
713
- await removePath(installed.workdir);
714
- }
715
- return;
716
- }
717
- }
718
- async updateSource(source, currentLock) {
719
- if (source.kind === "local") {
720
- return this.refreshLocalSourceCheckout(source, currentLock);
721
- }
722
- if (source.kind === "git") {
723
- await git(["pull", "--ff-only"], { cwd: currentLock.checkoutPath });
724
- const latestCommitSha = await git(["rev-parse", "HEAD"], {
725
- cwd: currentLock.checkoutPath,
726
- });
727
- return latestCommitSha !== currentLock.commitSha;
728
- }
729
- if (source.kind === "clawhub") {
730
- if (currentLock.versionMode === "pinned") {
731
- return false;
732
- }
733
- const installed = await installClawHubSkill(currentLock.packageSlug ?? currentLock.id);
734
- try {
735
- if (installed.resolvedVersion === currentLock.resolvedVersion) {
736
- return false;
737
- }
738
- await copyDirectory(installed.installedPath, currentLock.checkoutPath);
739
- return true;
740
- }
741
- finally {
742
- await removePath(installed.workdir);
743
- }
744
- }
745
- return false;
746
- }
747
- async refreshLocalSourceCheckout(source, currentLock) {
748
- const tempCheckoutPath = `${currentLock.checkoutPath}.${process.pid}.${crypto.randomUUID()}.refresh`;
749
- const backupPath = `${currentLock.checkoutPath}.${process.pid}.${crypto.randomUUID()}.backup`;
750
- try {
751
- if (!(await pathExists(source.locator))) {
752
- throw new Error(`Local source origin is missing at ${source.locator}.`);
753
- }
754
- await copyDirectory(source.locator, tempCheckoutPath);
755
- const nextHash = await hashDirectory(tempCheckoutPath);
756
- const checkoutExists = await pathExists(currentLock.checkoutPath);
757
- const currentHash = checkoutExists ? await hashDirectory(currentLock.checkoutPath) : undefined;
758
- if (checkoutExists && nextHash === currentLock.contentHash && currentHash === nextHash) {
759
- await removePath(tempCheckoutPath);
760
- return false;
761
- }
762
- if (checkoutExists) {
763
- await fs.rename(currentLock.checkoutPath, backupPath);
764
- try {
765
- await fs.rename(tempCheckoutPath, currentLock.checkoutPath);
766
- }
767
- catch (error) {
768
- await fs.rename(backupPath, currentLock.checkoutPath).catch(() => { });
769
- throw error;
770
- }
771
- finally {
772
- await removePath(backupPath).catch(() => { });
773
- }
774
- }
775
- else {
776
- await fs.rename(tempCheckoutPath, currentLock.checkoutPath);
777
- }
778
- return true;
779
- }
780
- catch (error) {
781
- await removePath(tempCheckoutPath).catch(() => { });
782
- await removePath(backupPath).catch(() => { });
783
- throw error;
784
- }
785
- }
786
- async readSourceSnapshot(kind, checkoutPath) {
787
- if (kind === "local") {
788
- return {
789
- contentHash: await hashDirectory(checkoutPath),
790
- };
791
- }
792
- if (kind === "git") {
793
- return {
794
- commitSha: await git(["rev-parse", "HEAD"], { cwd: checkoutPath }),
795
- };
796
- }
797
- if (kind === "clawhub") {
798
- const origin = await readJsonFile(path.join(checkoutPath, ".clawhub", "origin.json"), {});
799
- const contentHash = await hashDirectory(checkoutPath);
800
- return {
801
- ...(origin.slug ? { packageSlug: origin.slug } : {}),
802
- ...(origin.installedVersion ? { resolvedVersion: origin.installedVersion } : {}),
803
- contentHash,
804
- };
805
- }
806
- return {};
807
- }
808
- }
809
- //# sourceMappingURL=source-service.js.map