skill-flow 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -3
- package/README.zh.md +40 -3
- package/dist/adapters/channel-adapters.js +11 -3
- package/dist/adapters/channel-adapters.js.map +1 -1
- package/dist/cli.js +69 -37
- package/dist/cli.js.map +1 -1
- package/dist/domain/types.d.ts +54 -1
- package/dist/services/config-coordinator.d.ts +38 -0
- package/dist/services/config-coordinator.js +81 -0
- package/dist/services/config-coordinator.js.map +1 -0
- package/dist/services/doctor-service.d.ts +2 -0
- package/dist/services/doctor-service.js +62 -0
- package/dist/services/doctor-service.js.map +1 -1
- package/dist/services/inventory-service.d.ts +3 -1
- package/dist/services/inventory-service.js +12 -5
- package/dist/services/inventory-service.js.map +1 -1
- package/dist/services/skill-flow.d.ts +50 -26
- package/dist/services/skill-flow.js +502 -89
- package/dist/services/skill-flow.js.map +1 -1
- package/dist/services/source-service.d.ts +20 -10
- package/dist/services/source-service.js +359 -75
- package/dist/services/source-service.js.map +1 -1
- package/dist/services/workflow-service.d.ts +2 -2
- package/dist/services/workflow-service.js +17 -4
- package/dist/services/workflow-service.js.map +1 -1
- package/dist/services/workspace-bootstrap-service.d.ts +25 -0
- package/dist/services/workspace-bootstrap-service.js +140 -0
- package/dist/services/workspace-bootstrap-service.js.map +1 -0
- package/dist/state/store.d.ts +16 -0
- package/dist/state/store.js +93 -18
- package/dist/state/store.js.map +1 -1
- package/dist/tests/clawhub.test.d.ts +1 -0
- package/dist/tests/clawhub.test.js +63 -0
- package/dist/tests/clawhub.test.js.map +1 -0
- package/dist/tests/cli-utils.test.d.ts +1 -0
- package/dist/tests/cli-utils.test.js +15 -0
- package/dist/tests/cli-utils.test.js.map +1 -0
- package/dist/tests/config-coordinator.test.d.ts +1 -0
- package/dist/tests/config-coordinator.test.js +172 -0
- package/dist/tests/config-coordinator.test.js.map +1 -0
- package/dist/tests/config-integration.test.d.ts +1 -0
- package/dist/tests/config-integration.test.js +238 -0
- package/dist/tests/config-integration.test.js.map +1 -0
- package/dist/tests/config-ui-utils.test.d.ts +1 -0
- package/dist/tests/config-ui-utils.test.js +389 -0
- package/dist/tests/config-ui-utils.test.js.map +1 -0
- package/dist/tests/find-and-naming-utils.test.d.ts +1 -0
- package/dist/tests/find-and-naming-utils.test.js +127 -0
- package/dist/tests/find-and-naming-utils.test.js.map +1 -0
- package/dist/tests/skill-flow.test.js +334 -881
- package/dist/tests/skill-flow.test.js.map +1 -1
- package/dist/tests/source-lifecycle.test.d.ts +1 -0
- package/dist/tests/source-lifecycle.test.js +605 -0
- package/dist/tests/source-lifecycle.test.js.map +1 -0
- package/dist/tests/target-definitions.test.d.ts +1 -0
- package/dist/tests/target-definitions.test.js +51 -0
- package/dist/tests/target-definitions.test.js.map +1 -0
- package/dist/tests/test-helpers.d.ts +18 -0
- package/dist/tests/test-helpers.js +123 -0
- package/dist/tests/test-helpers.js.map +1 -0
- package/dist/tui/config-app.d.ts +147 -24
- package/dist/tui/config-app.js +1081 -335
- package/dist/tui/config-app.js.map +1 -1
- package/dist/tui/find-app.d.ts +1 -1
- package/dist/tui/find-app.js +36 -4
- package/dist/tui/find-app.js.map +1 -1
- package/dist/utils/clawhub.d.ts +3 -0
- package/dist/utils/clawhub.js +32 -3
- package/dist/utils/clawhub.js.map +1 -1
- package/dist/utils/cli.d.ts +1 -0
- package/dist/utils/cli.js +15 -0
- package/dist/utils/cli.js.map +1 -0
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.js +31 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +52 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/naming.d.ts +1 -0
- package/dist/utils/naming.js +7 -1
- package/dist/utils/naming.js.map +1 -1
- package/dist/utils/source-id.js +4 -0
- package/dist/utils/source-id.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
1
3
|
import path from "node:path";
|
|
2
4
|
import { copyDirectory, ensureDir, hashDirectory, pathExists, readJsonFile, removePath } from "../utils/fs.js";
|
|
3
5
|
import { installClawHubSkill, } from "../utils/clawhub.js";
|
|
@@ -13,10 +15,14 @@ export class SourceService {
|
|
|
13
15
|
this.inventoryService = inventoryService;
|
|
14
16
|
}
|
|
15
17
|
async addSource(locator, options = {}) {
|
|
16
|
-
await this.store.
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|
|
20
26
|
if (manifest.sources.some((source) => source.id === resolved.sourceId)) {
|
|
21
27
|
return fail({
|
|
22
28
|
code: "SOURCE_EXISTS",
|
|
@@ -31,11 +37,15 @@ export class SourceService {
|
|
|
31
37
|
catch (error) {
|
|
32
38
|
await removePath(checkoutPath);
|
|
33
39
|
return fail({
|
|
34
|
-
code: resolved.kind === "git"
|
|
40
|
+
code: resolved.kind === "git"
|
|
41
|
+
? "GIT_CLONE_FAILED"
|
|
42
|
+
: resolved.kind === "local"
|
|
43
|
+
? "LOCAL_IMPORT_FAILED"
|
|
44
|
+
: "CLAWHUB_FETCH_FAILED",
|
|
35
45
|
message: `Unable to fetch source '${resolved.locator}': ${String(error)}`,
|
|
36
46
|
});
|
|
37
47
|
}
|
|
38
|
-
const snapshot = await this.buildSnapshot(resolved.kind, resolved.sourceId, resolved.locator, resolved.displayName, checkoutPath, resolved.requestedPath);
|
|
48
|
+
const snapshot = await this.buildSnapshot(resolved.kind, resolved.sourceId, resolved.locator, resolved.displayName, checkoutPath, resolved.requestedPath, options);
|
|
39
49
|
if (!snapshot.ok) {
|
|
40
50
|
await removePath(checkoutPath);
|
|
41
51
|
return fail(snapshot.errors, snapshot.warnings);
|
|
@@ -44,8 +54,7 @@ export class SourceService {
|
|
|
44
54
|
manifest.bindings[resolved.sourceId] = { targets: {} };
|
|
45
55
|
lockFile.sources.push(snapshot.data.lock);
|
|
46
56
|
lockFile.leafInventory.push(...snapshot.data.leafs);
|
|
47
|
-
await this.store.
|
|
48
|
-
await this.store.writeLock(lockFile);
|
|
57
|
+
await this.store.writeState(manifest, lockFile);
|
|
49
58
|
return ok({
|
|
50
59
|
manifest: snapshot.data.manifest,
|
|
51
60
|
lock: snapshot.data.lock,
|
|
@@ -54,9 +63,7 @@ export class SourceService {
|
|
|
54
63
|
}, snapshot.warnings);
|
|
55
64
|
}
|
|
56
65
|
async updateSources(sourceIds) {
|
|
57
|
-
await this.store.
|
|
58
|
-
const manifest = await this.store.readManifest();
|
|
59
|
-
const lockFile = await this.store.readLock();
|
|
66
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
60
67
|
const selectedIds = sourceIds?.length
|
|
61
68
|
? sourceIds
|
|
62
69
|
: manifest.sources.map((source) => source.id);
|
|
@@ -70,55 +77,60 @@ export class SourceService {
|
|
|
70
77
|
message: `Skills group id '${sourceId}' is not registered.`,
|
|
71
78
|
});
|
|
72
79
|
}
|
|
73
|
-
let changed;
|
|
74
80
|
try {
|
|
75
|
-
|
|
81
|
+
const sourceChanged = await this.updateSource(source, currentLock);
|
|
82
|
+
const sourceMeta = {
|
|
83
|
+
...(source.requestedPath ? { requestedPath: source.requestedPath } : {}),
|
|
84
|
+
...(source.selectionMode ? { selectionMode: source.selectionMode } : {}),
|
|
85
|
+
};
|
|
86
|
+
if (!sourceChanged) {
|
|
87
|
+
updated.push({
|
|
88
|
+
sourceId,
|
|
89
|
+
changed: false,
|
|
90
|
+
addedLeafIds: [],
|
|
91
|
+
removedLeafIds: [],
|
|
92
|
+
invalidatedLeafIds: [],
|
|
93
|
+
diffs: [],
|
|
94
|
+
...sourceMeta,
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const snapshot = await this.buildSnapshot(source.kind, source.id, source.locator, source.displayName, currentLock.checkoutPath, source.requestedPath, {}, { allowEmptyLeafs: true });
|
|
99
|
+
if (!snapshot.ok) {
|
|
100
|
+
return fail(snapshot.errors, snapshot.warnings);
|
|
101
|
+
}
|
|
102
|
+
const diff = this.buildSourceUpdateDiff(source, lockFile.leafInventory.filter((leaf) => leaf.sourceId === sourceId), snapshot.data.leafs, snapshot.data.lock.invalidLeafs);
|
|
103
|
+
lockFile.sources = lockFile.sources.map((item) => item.id === sourceId ? snapshot.data.lock : item);
|
|
104
|
+
lockFile.leafInventory = [
|
|
105
|
+
...lockFile.leafInventory.filter((leaf) => leaf.sourceId !== sourceId),
|
|
106
|
+
...snapshot.data.leafs,
|
|
107
|
+
];
|
|
108
|
+
updated.push({
|
|
109
|
+
sourceId,
|
|
110
|
+
changed: sourceChanged,
|
|
111
|
+
addedLeafIds: diff.addedLeafIds,
|
|
112
|
+
removedLeafIds: diff.removedLeafIds,
|
|
113
|
+
invalidatedLeafIds: diff.invalidatedLeafIds,
|
|
114
|
+
diffs: diff.diffs,
|
|
115
|
+
...sourceMeta,
|
|
116
|
+
});
|
|
76
117
|
}
|
|
77
118
|
catch (error) {
|
|
78
119
|
return fail({
|
|
79
|
-
code: source.kind === "git"
|
|
120
|
+
code: source.kind === "git"
|
|
121
|
+
? "GIT_UPDATE_FAILED"
|
|
122
|
+
: source.kind === "local"
|
|
123
|
+
? "LOCAL_UPDATE_FAILED"
|
|
124
|
+
: "CLAWHUB_UPDATE_FAILED",
|
|
80
125
|
message: `Unable to update skills group id '${sourceId}': ${String(error)}`,
|
|
81
126
|
});
|
|
82
127
|
}
|
|
83
|
-
if (!changed) {
|
|
84
|
-
updated.push({
|
|
85
|
-
sourceId,
|
|
86
|
-
changed: false,
|
|
87
|
-
addedLeafIds: [],
|
|
88
|
-
removedLeafIds: [],
|
|
89
|
-
invalidatedLeafIds: [],
|
|
90
|
-
});
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const snapshot = await this.buildSnapshot(source.kind, source.id, source.locator, source.displayName, currentLock.checkoutPath, source.requestedPath, { allowEmptyLeafs: true });
|
|
94
|
-
if (!snapshot.ok) {
|
|
95
|
-
return fail(snapshot.errors, snapshot.warnings);
|
|
96
|
-
}
|
|
97
|
-
const previousLeafs = lockFile.leafInventory.filter((leaf) => leaf.sourceId === sourceId);
|
|
98
|
-
const previousLeafIds = new Set(previousLeafs.map((leaf) => leaf.id));
|
|
99
|
-
const nextLeafIds = new Set(snapshot.data.leafs.map((leaf) => leaf.id));
|
|
100
|
-
const previousInvalidPaths = new Set(currentLock.invalidLeafs.map((leaf) => leaf.path));
|
|
101
|
-
const nextInvalidPaths = new Set(snapshot.data.lock.invalidLeafs.map((leaf) => leaf.path));
|
|
102
|
-
lockFile.sources = lockFile.sources.map((item) => item.id === sourceId ? snapshot.data.lock : item);
|
|
103
|
-
lockFile.leafInventory = [
|
|
104
|
-
...lockFile.leafInventory.filter((leaf) => leaf.sourceId !== sourceId),
|
|
105
|
-
...snapshot.data.leafs,
|
|
106
|
-
];
|
|
107
|
-
updated.push({
|
|
108
|
-
sourceId,
|
|
109
|
-
changed: true,
|
|
110
|
-
addedLeafIds: [...nextLeafIds].filter((id) => !previousLeafIds.has(id)),
|
|
111
|
-
removedLeafIds: [...previousLeafIds].filter((id) => !nextLeafIds.has(id)),
|
|
112
|
-
invalidatedLeafIds: [...nextInvalidPaths].filter((value) => !previousInvalidPaths.has(value)),
|
|
113
|
-
});
|
|
114
128
|
}
|
|
115
|
-
await this.store.
|
|
129
|
+
await this.store.writeState(manifest, lockFile);
|
|
116
130
|
return ok({ updated });
|
|
117
131
|
}
|
|
118
132
|
async removeSource(sourceIds) {
|
|
119
|
-
await this.store.
|
|
120
|
-
const manifest = await this.store.readManifest();
|
|
121
|
-
const lockFile = await this.store.readLock();
|
|
133
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
122
134
|
const removed = [];
|
|
123
135
|
for (const sourceId of sourceIds) {
|
|
124
136
|
const currentSource = manifest.sources.find((source) => source.id === sourceId);
|
|
@@ -139,14 +151,11 @@ export class SourceService {
|
|
|
139
151
|
}
|
|
140
152
|
removed.push(sourceId);
|
|
141
153
|
}
|
|
142
|
-
await this.store.
|
|
143
|
-
await this.store.writeLock(lockFile);
|
|
154
|
+
await this.store.writeState(manifest, lockFile);
|
|
144
155
|
return ok({ removed });
|
|
145
156
|
}
|
|
146
157
|
async reconcileInventory(sourceIds, options = {}) {
|
|
147
|
-
await this.store.
|
|
148
|
-
const manifest = await this.store.readManifest();
|
|
149
|
-
const lockFile = await this.store.readLock();
|
|
158
|
+
const { manifest, lockFile } = await this.store.readState();
|
|
150
159
|
const selectedIds = sourceIds?.length
|
|
151
160
|
? sourceIds
|
|
152
161
|
: manifest.sources.map((source) => source.id);
|
|
@@ -163,7 +172,7 @@ export class SourceService {
|
|
|
163
172
|
!this.needsInventoryReconcile(sourceId, sourceLeafs, sourceDeployments)) {
|
|
164
173
|
continue;
|
|
165
174
|
}
|
|
166
|
-
const snapshot = await this.buildSnapshot(source.kind, source.id, source.locator, source.displayName, currentLock.checkoutPath, source.requestedPath, { allowEmptyLeafs: true });
|
|
175
|
+
const snapshot = await this.buildSnapshot(source.kind, source.id, source.locator, source.displayName, currentLock.checkoutPath, source.requestedPath, {}, { allowEmptyLeafs: true });
|
|
167
176
|
if (!snapshot.ok) {
|
|
168
177
|
return fail(snapshot.errors, snapshot.warnings);
|
|
169
178
|
}
|
|
@@ -196,8 +205,7 @@ export class SourceService {
|
|
|
196
205
|
updatedSourceIds.push(sourceId);
|
|
197
206
|
}
|
|
198
207
|
if (updatedSourceIds.length > 0) {
|
|
199
|
-
await this.store.
|
|
200
|
-
await this.store.writeLock(lockFile);
|
|
208
|
+
await this.store.writeState(manifest, lockFile);
|
|
201
209
|
}
|
|
202
210
|
return ok({ updatedSourceIds });
|
|
203
211
|
}
|
|
@@ -206,7 +214,148 @@ export class SourceService {
|
|
|
206
214
|
const hasLegacyTargetNames = sourceDeployments.some((deployment) => path.basename(deployment.targetPath).startsWith(`${sourceId}--`));
|
|
207
215
|
return hasGeneratedLeafs || hasLegacyTargetNames;
|
|
208
216
|
}
|
|
209
|
-
|
|
217
|
+
buildSourceUpdateDiff(source, previousLeafs, nextLeafs, nextInvalidLeafs) {
|
|
218
|
+
const requestedPath = this.normalizeRequestedPath(source.requestedPath);
|
|
219
|
+
const requestedPathOption = requestedPath ? { requestedPath } : {};
|
|
220
|
+
const diffs = [];
|
|
221
|
+
const addedLeafIds = [];
|
|
222
|
+
const removedLeafIds = [];
|
|
223
|
+
const invalidatedLeafIds = [];
|
|
224
|
+
const matchedPreviousIds = new Set();
|
|
225
|
+
const matchedNextIds = new Set();
|
|
226
|
+
const nextById = new Map(nextLeafs.map((leaf) => [leaf.id, leaf]));
|
|
227
|
+
const nextInvalidPaths = new Set(nextInvalidLeafs.map((leaf) => leaf.path));
|
|
228
|
+
for (const previousLeaf of previousLeafs) {
|
|
229
|
+
const nextLeaf = nextById.get(previousLeaf.id);
|
|
230
|
+
if (!nextLeaf || previousLeaf.contentHash === nextLeaf.contentHash) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
diffs.push(this.createDiffItem("changed", source.id, nextLeaf, {
|
|
234
|
+
...requestedPathOption,
|
|
235
|
+
previousLeafId: previousLeaf.id,
|
|
236
|
+
previousRelativePath: previousLeaf.relativePath,
|
|
237
|
+
previousContentHash: previousLeaf.contentHash,
|
|
238
|
+
}));
|
|
239
|
+
matchedPreviousIds.add(previousLeaf.id);
|
|
240
|
+
matchedNextIds.add(nextLeaf.id);
|
|
241
|
+
}
|
|
242
|
+
const previousByHash = new Map();
|
|
243
|
+
const nextByHash = new Map();
|
|
244
|
+
for (const previousLeaf of previousLeafs) {
|
|
245
|
+
if (matchedPreviousIds.has(previousLeaf.id)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const list = previousByHash.get(previousLeaf.contentHash) ?? [];
|
|
249
|
+
list.push(previousLeaf);
|
|
250
|
+
previousByHash.set(previousLeaf.contentHash, list);
|
|
251
|
+
}
|
|
252
|
+
for (const nextLeaf of nextLeafs) {
|
|
253
|
+
if (matchedNextIds.has(nextLeaf.id)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const list = nextByHash.get(nextLeaf.contentHash) ?? [];
|
|
257
|
+
list.push(nextLeaf);
|
|
258
|
+
nextByHash.set(nextLeaf.contentHash, list);
|
|
259
|
+
}
|
|
260
|
+
for (const contentHash of [...previousByHash.keys()].sort()) {
|
|
261
|
+
const previousGroup = previousByHash.get(contentHash) ?? [];
|
|
262
|
+
const nextGroup = nextByHash.get(contentHash) ?? [];
|
|
263
|
+
if (previousGroup.length !== 1 || nextGroup.length !== 1) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const previousLeaf = previousGroup[0];
|
|
267
|
+
const nextLeaf = nextGroup[0];
|
|
268
|
+
if (previousLeaf.id === nextLeaf.id ||
|
|
269
|
+
!this.canClassifyAsMoved(requestedPath, previousLeaf.relativePath, nextLeaf.relativePath)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
diffs.push(this.createDiffItem("moved", source.id, nextLeaf, {
|
|
273
|
+
...requestedPathOption,
|
|
274
|
+
previousLeafId: previousLeaf.id,
|
|
275
|
+
previousRelativePath: previousLeaf.relativePath,
|
|
276
|
+
previousContentHash: previousLeaf.contentHash,
|
|
277
|
+
}));
|
|
278
|
+
matchedPreviousIds.add(previousLeaf.id);
|
|
279
|
+
matchedNextIds.add(nextLeaf.id);
|
|
280
|
+
}
|
|
281
|
+
for (const previousLeaf of previousLeafs) {
|
|
282
|
+
if (matchedPreviousIds.has(previousLeaf.id)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (nextInvalidPaths.has(previousLeaf.relativePath)) {
|
|
286
|
+
diffs.push(this.createDiffItem("invalidated", source.id, previousLeaf, {
|
|
287
|
+
...requestedPathOption,
|
|
288
|
+
previousLeafId: previousLeaf.id,
|
|
289
|
+
previousRelativePath: previousLeaf.relativePath,
|
|
290
|
+
previousContentHash: previousLeaf.contentHash,
|
|
291
|
+
}));
|
|
292
|
+
invalidatedLeafIds.push(previousLeaf.id);
|
|
293
|
+
matchedPreviousIds.add(previousLeaf.id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const previousLeaf of previousLeafs) {
|
|
297
|
+
if (matchedPreviousIds.has(previousLeaf.id)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
diffs.push(this.createDiffItem("removed", source.id, previousLeaf, {
|
|
301
|
+
...requestedPathOption,
|
|
302
|
+
previousLeafId: previousLeaf.id,
|
|
303
|
+
previousRelativePath: previousLeaf.relativePath,
|
|
304
|
+
previousContentHash: previousLeaf.contentHash,
|
|
305
|
+
}));
|
|
306
|
+
removedLeafIds.push(previousLeaf.id);
|
|
307
|
+
matchedPreviousIds.add(previousLeaf.id);
|
|
308
|
+
}
|
|
309
|
+
for (const nextLeaf of nextLeafs) {
|
|
310
|
+
if (matchedNextIds.has(nextLeaf.id)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
diffs.push(this.createDiffItem("added", source.id, nextLeaf, requestedPathOption));
|
|
314
|
+
addedLeafIds.push(nextLeaf.id);
|
|
315
|
+
matchedNextIds.add(nextLeaf.id);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
diffs,
|
|
319
|
+
addedLeafIds,
|
|
320
|
+
removedLeafIds,
|
|
321
|
+
invalidatedLeafIds,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
createDiffItem(kind, sourceId, leaf, extras = {}) {
|
|
325
|
+
return {
|
|
326
|
+
kind,
|
|
327
|
+
sourceId,
|
|
328
|
+
leafId: leaf.id,
|
|
329
|
+
relativePath: leaf.relativePath,
|
|
330
|
+
contentHash: leaf.contentHash,
|
|
331
|
+
...(extras.requestedPath ? { requestedPath: extras.requestedPath } : {}),
|
|
332
|
+
...(extras.previousLeafId ? { previousLeafId: extras.previousLeafId } : {}),
|
|
333
|
+
...(extras.previousRelativePath ? { previousRelativePath: extras.previousRelativePath } : {}),
|
|
334
|
+
...(extras.previousContentHash ? { previousContentHash: extras.previousContentHash } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
canClassifyAsMoved(requestedPath, previousRelativePath, nextRelativePath) {
|
|
338
|
+
if (!requestedPath) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
return (this.isWithinRequestedPath(previousRelativePath, requestedPath) &&
|
|
342
|
+
this.isWithinRequestedPath(nextRelativePath, requestedPath));
|
|
343
|
+
}
|
|
344
|
+
normalizeRequestedPath(requestedPath) {
|
|
345
|
+
if (!requestedPath) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
const normalized = requestedPath.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
349
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
350
|
+
}
|
|
351
|
+
isWithinRequestedPath(relativePath, requestedPath) {
|
|
352
|
+
const normalizedPath = this.normalizeRequestedPath(requestedPath);
|
|
353
|
+
if (!normalizedPath) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return (relativePath === normalizedPath || relativePath.startsWith(`${normalizedPath}/`));
|
|
357
|
+
}
|
|
358
|
+
async buildSnapshot(kind, sourceId, locator, displayName, checkoutPath, requestedPathOrOptions = undefined, addOptions = {}, maybeOptions = {}) {
|
|
210
359
|
const requestedPath = typeof requestedPathOrOptions === "string" ? requestedPathOrOptions : undefined;
|
|
211
360
|
const options = typeof requestedPathOrOptions === "string"
|
|
212
361
|
? maybeOptions
|
|
@@ -222,11 +371,14 @@ export class SourceService {
|
|
|
222
371
|
})));
|
|
223
372
|
if (((requestedPath && requestedMatches.length === 0) || scanned.leafs.length === 0) &&
|
|
224
373
|
!options.allowEmptyLeafs) {
|
|
374
|
+
const emptyReason = scanned.skillFileCount === 0
|
|
375
|
+
? " No SKILL.md files were found."
|
|
376
|
+
: "";
|
|
225
377
|
return fail({
|
|
226
378
|
code: requestedPath ? "SOURCE_PATH_NOT_FOUND" : "NO_VALID_LEAFS",
|
|
227
379
|
message: requestedPath
|
|
228
380
|
? `Source '${displayName}' does not contain a valid skill at '${requestedPath}'.`
|
|
229
|
-
: `Source '${displayName}' has no valid skills
|
|
381
|
+
: `Source '${displayName}' has no valid skills.${emptyReason}`,
|
|
230
382
|
}, scanned.invalidLeafs.map((leaf) => ({
|
|
231
383
|
code: "INVALID_LEAF",
|
|
232
384
|
message: `${leaf.path}: ${leaf.reason}`,
|
|
@@ -240,6 +392,11 @@ export class SourceService {
|
|
|
240
392
|
displayName,
|
|
241
393
|
addedAt: new Date().toISOString(),
|
|
242
394
|
...(requestedPath ? { requestedPath } : {}),
|
|
395
|
+
...(addOptions.selectionMode ? { selectionMode: addOptions.selectionMode } : {}),
|
|
396
|
+
...(addOptions.originLocator ? { originLocator: addOptions.originLocator } : {}),
|
|
397
|
+
...(addOptions.originRequestedPath
|
|
398
|
+
? { originRequestedPath: addOptions.originRequestedPath }
|
|
399
|
+
: {}),
|
|
243
400
|
},
|
|
244
401
|
lock: {
|
|
245
402
|
id: sourceId,
|
|
@@ -251,6 +408,11 @@ export class SourceService {
|
|
|
251
408
|
leafIds: scanned.leafs.map((leaf) => leaf.id),
|
|
252
409
|
invalidLeafs: scanned.invalidLeafs,
|
|
253
410
|
...sourceMetadata,
|
|
411
|
+
...(addOptions.originBranch ? { originBranch: addOptions.originBranch } : {}),
|
|
412
|
+
...(addOptions.importedFromTargets
|
|
413
|
+
? { importedFromTargets: addOptions.importedFromTargets }
|
|
414
|
+
: {}),
|
|
415
|
+
...(addOptions.importMode ? { importMode: addOptions.importMode } : {}),
|
|
254
416
|
...(kind === "clawhub"
|
|
255
417
|
? {
|
|
256
418
|
versionMode: locator.includes("@") ? "pinned" : "floating",
|
|
@@ -260,6 +422,10 @@ export class SourceService {
|
|
|
260
422
|
leafs: scanned.leafs,
|
|
261
423
|
}, [
|
|
262
424
|
...metadataWarnings,
|
|
425
|
+
...scanned.duplicateLeafs.map((leaf) => ({
|
|
426
|
+
code: "DUPLICATE_LEAF",
|
|
427
|
+
message: `${leaf.path}: Duplicate skill content skipped because ${leaf.keptPath} was discovered first`,
|
|
428
|
+
})),
|
|
263
429
|
...scanned.invalidLeafs.map((leaf) => ({
|
|
264
430
|
code: "INVALID_LEAF",
|
|
265
431
|
message: `${leaf.path}: ${leaf.reason}`,
|
|
@@ -268,12 +434,12 @@ export class SourceService {
|
|
|
268
434
|
}
|
|
269
435
|
async normalizeLocator(locator) {
|
|
270
436
|
const trimmed = locator.trim();
|
|
271
|
-
if (/^[^/\s]+\/[^/\s]+$/.test(trimmed)) {
|
|
272
|
-
return `https://github.com/${trimmed}.git`;
|
|
273
|
-
}
|
|
274
437
|
if (trimmed.startsWith("git@") || trimmed.startsWith("http")) {
|
|
275
438
|
return trimmed;
|
|
276
439
|
}
|
|
440
|
+
if (/^[^/\s]+\/[^/\s]+$/.test(trimmed)) {
|
|
441
|
+
return `https://github.com/${trimmed}.git`;
|
|
442
|
+
}
|
|
277
443
|
const resolvedPath = path.resolve(trimmed);
|
|
278
444
|
if (await pathExists(resolvedPath)) {
|
|
279
445
|
return resolvedPath;
|
|
@@ -282,17 +448,27 @@ export class SourceService {
|
|
|
282
448
|
}
|
|
283
449
|
async resolveSource(locator, options) {
|
|
284
450
|
const trimmed = locator.trim();
|
|
451
|
+
const resolvedPath = path.resolve(trimmed);
|
|
452
|
+
if (await pathExists(resolvedPath)) {
|
|
453
|
+
return {
|
|
454
|
+
kind: "local",
|
|
455
|
+
locator: resolvedPath,
|
|
456
|
+
localPath: resolvedPath,
|
|
457
|
+
displayName: options.displayNameOverride ?? deriveDisplayName(resolvedPath),
|
|
458
|
+
sourceId: options.sourceIdOverride ?? deriveSourceId(resolvedPath),
|
|
459
|
+
...(options.path ? { requestedPath: options.path } : {}),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
285
462
|
const treeLocator = this.parseGitHubTreeLocator(trimmed);
|
|
286
463
|
if (treeLocator) {
|
|
464
|
+
const requestedPath = this.joinRequestedPaths(treeLocator.requestedPath, options.path);
|
|
287
465
|
return {
|
|
288
466
|
kind: "git",
|
|
289
467
|
locator: treeLocator.repoLocator,
|
|
290
468
|
gitLocator: await this.normalizeLocator(treeLocator.repoLocator),
|
|
291
|
-
displayName: deriveDisplayName(treeLocator.repoLocator),
|
|
292
|
-
sourceId: deriveSourceId(treeLocator.repoLocator),
|
|
293
|
-
...(
|
|
294
|
-
? { requestedPath: options.path ?? treeLocator.requestedPath }
|
|
295
|
-
: {}),
|
|
469
|
+
displayName: options.displayNameOverride ?? deriveDisplayName(treeLocator.repoLocator),
|
|
470
|
+
sourceId: options.sourceIdOverride ?? deriveSourceId(treeLocator.repoLocator),
|
|
471
|
+
...(requestedPath ? { requestedPath } : {}),
|
|
296
472
|
};
|
|
297
473
|
}
|
|
298
474
|
const clawhubMatch = trimmed.match(/^clawhub:([^@\s]+)(?:@(.+))?$/);
|
|
@@ -306,8 +482,8 @@ export class SourceService {
|
|
|
306
482
|
? {
|
|
307
483
|
kind: "clawhub",
|
|
308
484
|
locator: trimmed,
|
|
309
|
-
displayName: deriveDisplayName(trimmed),
|
|
310
|
-
sourceId: deriveSourceId(trimmed),
|
|
485
|
+
displayName: options.displayNameOverride ?? deriveDisplayName(trimmed),
|
|
486
|
+
sourceId: options.sourceIdOverride ?? deriveSourceId(trimmed),
|
|
311
487
|
...(options.path ? { requestedPath: options.path } : {}),
|
|
312
488
|
clawhubSlug: slug,
|
|
313
489
|
requestedVersion: version,
|
|
@@ -316,8 +492,8 @@ export class SourceService {
|
|
|
316
492
|
: {
|
|
317
493
|
kind: "clawhub",
|
|
318
494
|
locator: trimmed,
|
|
319
|
-
displayName: deriveDisplayName(trimmed),
|
|
320
|
-
sourceId: deriveSourceId(trimmed),
|
|
495
|
+
displayName: options.displayNameOverride ?? deriveDisplayName(trimmed),
|
|
496
|
+
sourceId: options.sourceIdOverride ?? deriveSourceId(trimmed),
|
|
321
497
|
...(options.path ? { requestedPath: options.path } : {}),
|
|
322
498
|
clawhubSlug: slug,
|
|
323
499
|
versionMode: "floating",
|
|
@@ -327,11 +503,53 @@ export class SourceService {
|
|
|
327
503
|
kind: "git",
|
|
328
504
|
locator,
|
|
329
505
|
gitLocator: await this.normalizeLocator(locator),
|
|
330
|
-
displayName: deriveDisplayName(locator),
|
|
331
|
-
sourceId: deriveSourceId(locator),
|
|
506
|
+
displayName: options.displayNameOverride ?? deriveDisplayName(locator),
|
|
507
|
+
sourceId: options.sourceIdOverride ?? deriveSourceId(locator),
|
|
332
508
|
...(options.path ? { requestedPath: options.path } : {}),
|
|
333
509
|
};
|
|
334
510
|
}
|
|
511
|
+
resolveUniqueLocalSource(resolved, existingSources) {
|
|
512
|
+
if (resolved.kind !== "local" || !resolved.localPath) {
|
|
513
|
+
return resolved;
|
|
514
|
+
}
|
|
515
|
+
if (existingSources.some((source) => source.kind === "local" && path.resolve(source.locator) === resolved.localPath)) {
|
|
516
|
+
return resolved;
|
|
517
|
+
}
|
|
518
|
+
const folderName = path.basename(resolved.localPath);
|
|
519
|
+
const parentFolderName = path.basename(path.dirname(resolved.localPath));
|
|
520
|
+
const displayCandidates = [
|
|
521
|
+
folderName,
|
|
522
|
+
`${parentFolderName}_${folderName}`,
|
|
523
|
+
];
|
|
524
|
+
const takenIds = new Set(existingSources.map((source) => source.id));
|
|
525
|
+
const takenLabels = new Set(existingSources
|
|
526
|
+
.filter((source) => source.kind === "local")
|
|
527
|
+
.map((source) => source.displayName));
|
|
528
|
+
for (const candidate of displayCandidates) {
|
|
529
|
+
const sourceId = deriveSourceId(candidate);
|
|
530
|
+
if (!takenIds.has(sourceId) && !takenLabels.has(candidate)) {
|
|
531
|
+
return {
|
|
532
|
+
...resolved,
|
|
533
|
+
displayName: candidate,
|
|
534
|
+
sourceId,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const baseDisplayName = `${parentFolderName}_${folderName}`;
|
|
539
|
+
let index = 2;
|
|
540
|
+
while (true) {
|
|
541
|
+
const displayName = `${baseDisplayName}_${index}`;
|
|
542
|
+
const sourceId = deriveSourceId(displayName);
|
|
543
|
+
if (!takenIds.has(sourceId) && !takenLabels.has(displayName)) {
|
|
544
|
+
return {
|
|
545
|
+
...resolved,
|
|
546
|
+
displayName,
|
|
547
|
+
sourceId,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
index += 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
335
553
|
parseGitHubTreeLocator(locator) {
|
|
336
554
|
try {
|
|
337
555
|
const url = new URL(locator);
|
|
@@ -357,15 +575,34 @@ export class SourceService {
|
|
|
357
575
|
return null;
|
|
358
576
|
}
|
|
359
577
|
}
|
|
578
|
+
joinRequestedPaths(basePath, childPath) {
|
|
579
|
+
const normalizedBase = this.normalizeRequestedPath(basePath);
|
|
580
|
+
const normalizedChild = this.normalizeRequestedPath(childPath);
|
|
581
|
+
if (!normalizedBase) {
|
|
582
|
+
return normalizedChild;
|
|
583
|
+
}
|
|
584
|
+
if (!normalizedChild) {
|
|
585
|
+
return normalizedBase;
|
|
586
|
+
}
|
|
587
|
+
if (normalizedChild === normalizedBase ||
|
|
588
|
+
normalizedChild.startsWith(`${normalizedBase}/`)) {
|
|
589
|
+
return normalizedChild;
|
|
590
|
+
}
|
|
591
|
+
return `${normalizedBase}/${normalizedChild}`;
|
|
592
|
+
}
|
|
360
593
|
findRequestedLeafs(leafs, requestedPath) {
|
|
361
|
-
|
|
594
|
+
const normalizedPath = this.normalizeRequestedPath(requestedPath);
|
|
595
|
+
if (!normalizedPath) {
|
|
362
596
|
return leafs;
|
|
363
597
|
}
|
|
364
|
-
const normalizedPath = requestedPath.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
365
598
|
return leafs.filter((leaf) => leaf.relativePath === normalizedPath ||
|
|
366
599
|
leaf.relativePath.startsWith(`${normalizedPath}/`));
|
|
367
600
|
}
|
|
368
601
|
async fetchSource(source, checkoutPath) {
|
|
602
|
+
if (source.kind === "local") {
|
|
603
|
+
await copyDirectory(source.localPath, checkoutPath);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
369
606
|
if (source.kind === "git") {
|
|
370
607
|
await git(["clone", "--depth", "1", source.gitLocator, checkoutPath]);
|
|
371
608
|
return;
|
|
@@ -382,6 +619,9 @@ export class SourceService {
|
|
|
382
619
|
}
|
|
383
620
|
}
|
|
384
621
|
async updateSource(source, currentLock) {
|
|
622
|
+
if (source.kind === "local") {
|
|
623
|
+
return this.refreshLocalSourceCheckout(source, currentLock);
|
|
624
|
+
}
|
|
385
625
|
if (source.kind === "git") {
|
|
386
626
|
await git(["pull", "--ff-only"], { cwd: currentLock.checkoutPath });
|
|
387
627
|
const latestCommitSha = await git(["rev-parse", "HEAD"], {
|
|
@@ -407,7 +647,51 @@ export class SourceService {
|
|
|
407
647
|
}
|
|
408
648
|
return false;
|
|
409
649
|
}
|
|
650
|
+
async refreshLocalSourceCheckout(source, currentLock) {
|
|
651
|
+
const tempCheckoutPath = `${currentLock.checkoutPath}.${process.pid}.${crypto.randomUUID()}.refresh`;
|
|
652
|
+
const backupPath = `${currentLock.checkoutPath}.${process.pid}.${crypto.randomUUID()}.backup`;
|
|
653
|
+
try {
|
|
654
|
+
if (!(await pathExists(source.locator))) {
|
|
655
|
+
throw new Error(`Local source origin is missing at ${source.locator}.`);
|
|
656
|
+
}
|
|
657
|
+
await copyDirectory(source.locator, tempCheckoutPath);
|
|
658
|
+
const nextHash = await hashDirectory(tempCheckoutPath);
|
|
659
|
+
const checkoutExists = await pathExists(currentLock.checkoutPath);
|
|
660
|
+
const currentHash = checkoutExists ? await hashDirectory(currentLock.checkoutPath) : undefined;
|
|
661
|
+
if (checkoutExists && nextHash === currentLock.contentHash && currentHash === nextHash) {
|
|
662
|
+
await removePath(tempCheckoutPath);
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (checkoutExists) {
|
|
666
|
+
await fs.rename(currentLock.checkoutPath, backupPath);
|
|
667
|
+
try {
|
|
668
|
+
await fs.rename(tempCheckoutPath, currentLock.checkoutPath);
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
await fs.rename(backupPath, currentLock.checkoutPath).catch(() => { });
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
await removePath(backupPath).catch(() => { });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
await fs.rename(tempCheckoutPath, currentLock.checkoutPath);
|
|
680
|
+
}
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
await removePath(tempCheckoutPath).catch(() => { });
|
|
685
|
+
await removePath(backupPath).catch(() => { });
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
410
689
|
async readSourceSnapshot(kind, checkoutPath) {
|
|
690
|
+
if (kind === "local") {
|
|
691
|
+
return {
|
|
692
|
+
contentHash: await hashDirectory(checkoutPath),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
411
695
|
if (kind === "git") {
|
|
412
696
|
return {
|
|
413
697
|
commitSha: await git(["rev-parse", "HEAD"], { cwd: checkoutPath }),
|