@michengai/dsh-skills-manager 1.1.19 → 1.1.21
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/CHANGELOG.md +13 -0
- package/CHANGELOG.zh-CN.md +13 -0
- package/README.md +3 -3
- package/README.zh-CN.md +2 -2
- package/lib/catalog-skill.js +142 -0
- package/lib/clawhub-catalog.js +134 -0
- package/lib/client.js +97 -83
- package/lib/core.js +140 -37
- package/lib/index.js +51 -15
- package/lib/repositories.js +370 -57
- package/lib/repository-updates.js +8 -2
- package/lib/skills-sh-catalog.js +92 -0
- package/package.json +9 -9
package/lib/repositories.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { unzipSync } from "fflate";
|
|
5
|
-
import { managerHomePath, resolveWorkspaceTarget, repositoryDestinations, trashRootPath, listTrash, restoreTrash as restoreSkillTrash, parseSkillDoc, importUploadedSkill, deleteSkill, setSkillEnabled, state, createWorkspaceIndex } from "./core.js";
|
|
5
|
+
import { managerHomePath, resolveWorkspaceTarget, repositoryDestinations, trashRootPath, listTrash, restoreTrash as restoreSkillTrash, permanentlyDeleteTrash, parseSkillDoc, importUploadedSkill, deleteSkill, setSkillEnabled, readManagerState, state, createWorkspaceIndex } from "./core.js";
|
|
6
6
|
import { createRepositoryUpdater, fileIndex, signature, readSkillTree, repositoryInstallRoot, recordInstallRoot, selectInstallRecord } from "./repository-updates.js";
|
|
7
7
|
const LIMIT = 32 << 20;
|
|
8
|
+
const STATE_LIMIT = 16 << 20;
|
|
9
|
+
const STATE_READ_LIMIT = 64 << 20;
|
|
8
10
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
9
11
|
function failure(message, code = "error.repo.invalid", params) {
|
|
10
12
|
return Object.assign(new Error(message), { code, statusCode: 400, ...params && typeof params === "object" && !Array.isArray(params) ? { params } : {} });
|
|
@@ -42,6 +44,34 @@ function validateStoredState(input) {
|
|
|
42
44
|
safePath(entry.path);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
47
|
+
if (value.catalogInstalls !== void 0) {
|
|
48
|
+
if (!Array.isArray(value.catalogInstalls)) throw failure("\u76EE\u5F55\u5B89\u88C5\u72B6\u6001\u683C\u5F0F\u65E0\u6548");
|
|
49
|
+
for (const item of value.catalogInstalls) {
|
|
50
|
+
if (typeof item === "string") {
|
|
51
|
+
if (!/^(?:clawhub|skillsh):[A-Za-z0-9][A-Za-z0-9._-]{0,127}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}){1,2}$/.test(item)) throw failure("\u76EE\u5F55\u5B89\u88C5\u8BB0\u5F55\u683C\u5F0F\u65E0\u6548");
|
|
52
|
+
} else if (item && typeof item === "object") {
|
|
53
|
+
if (typeof item.id !== "string" || !/^(?:clawhub|skillsh):[A-Za-z0-9][A-Za-z0-9._-]{0,127}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}){1,2}$/.test(item.id)) throw failure("\u76EE\u5F55\u5B89\u88C5\u8BB0\u5F55\u683C\u5F0F\u65E0\u6548");
|
|
54
|
+
if (typeof item.name !== "string" || !item.name.trim() || item.name.length > 128) throw failure("\u76EE\u5F55\u5B89\u88C5\u6280\u80FD\u540D\u79F0\u65E0\u6548");
|
|
55
|
+
safePath(item.name);
|
|
56
|
+
if (item.name.includes("/")) throw failure("\u76EE\u5F55\u5B89\u88C5\u6280\u80FD\u540D\u79F0\u65E0\u6548");
|
|
57
|
+
if (item.root !== void 0) repositoryInstallRoot(item.root);
|
|
58
|
+
if (item.workspace !== void 0 && (item.root !== "dsh" || typeof item.workspace !== "string" || !item.workspace || item.workspace.length > 1024)) throw failure("\u76EE\u5F55\u5B89\u88C5\u5DE5\u4F5C\u533A\u65E0\u6548");
|
|
59
|
+
if (item.trashId !== void 0 && (typeof item.trashId !== "string" || !/^\d+-[a-f0-9-]{36}$/.test(item.trashId))) throw failure("\u76EE\u5F55\u56DE\u6536\u8BB0\u5F55\u65E0\u6548");
|
|
60
|
+
if (item.restoreFingerprint !== void 0 && (!item.trashId || !/^[a-f0-9]{64}$/.test(item.restoreFingerprint))) throw failure("\u76EE\u5F55\u6062\u590D\u8BB0\u5F55\u65E0\u6548");
|
|
61
|
+
if (item.complete !== void 0 && typeof item.complete !== "boolean") throw failure("\u76EE\u5F55\u5B89\u88C5\u786E\u8BA4\u72B6\u6001\u65E0\u6548");
|
|
62
|
+
if (item.complete === false && (!item.root || !item.files?.length)) throw failure("\u76EE\u5F55\u5B89\u88C5\u610F\u56FE\u65E0\u6548");
|
|
63
|
+
if (item.files !== void 0) {
|
|
64
|
+
if (!Array.isArray(item.files) || !item.files.length || item.files.length > 1e3) throw failure("\u76EE\u5F55\u5B89\u88C5\u6458\u8981\u65E0\u6548");
|
|
65
|
+
for (const file of item.files) {
|
|
66
|
+
if (!file || !/^[a-f0-9]{64}$/.test(file.hash)) throw failure("\u76EE\u5F55\u5B89\u88C5\u6458\u8981\u65E0\u6548");
|
|
67
|
+
safePath(file.path);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
throw failure("\u76EE\u5F55\u5B89\u88C5\u8BB0\u5F55\u683C\u5F0F\u65E0\u6548");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
45
75
|
}
|
|
46
76
|
function parseRepositoryInput(input = {}) {
|
|
47
77
|
if (!input || typeof input !== "object" || Array.isArray(input)) throw failure("\u4ED3\u5E93\u53C2\u6570\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
@@ -108,6 +138,7 @@ async function readResponse(response, limit) {
|
|
|
108
138
|
function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registeredWorkspaces, onWorkspaceIndexBuild } = {}) {
|
|
109
139
|
const file = join(managerHomePath(), "repositories.json");
|
|
110
140
|
let queue = Promise.resolve();
|
|
141
|
+
let catalogWarnings = [];
|
|
111
142
|
let activeIndex;
|
|
112
143
|
const serialize = (task, recover = true) => {
|
|
113
144
|
const run = async () => {
|
|
@@ -116,7 +147,7 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
116
147
|
activeIndex = index;
|
|
117
148
|
try {
|
|
118
149
|
if (recover) await recoverPending();
|
|
119
|
-
return task();
|
|
150
|
+
return await task();
|
|
120
151
|
} finally {
|
|
121
152
|
activeIndex = previous;
|
|
122
153
|
}
|
|
@@ -137,7 +168,7 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
137
168
|
const homeInfo = await fs.lstat(managerHomePath());
|
|
138
169
|
if (homeInfo.isSymbolicLink() || !homeInfo.isDirectory()) throw failure("\u4ED3\u5E93\u72B6\u6001\u76EE\u5F55\u4E0D\u5B89\u5168");
|
|
139
170
|
const info = await fs.lstat(file);
|
|
140
|
-
if (info.isSymbolicLink() || !info.isFile() || info.size >
|
|
171
|
+
if (info.isSymbolicLink() || !info.isFile() || info.size > STATE_READ_LIMIT) throw failure("\u4ED3\u5E93\u72B6\u6001\u6587\u4EF6\u4E0D\u5B89\u5168");
|
|
141
172
|
const value = JSON.parse(await fs.readFile(file, "utf8"));
|
|
142
173
|
validateStoredState(value);
|
|
143
174
|
return value;
|
|
@@ -147,10 +178,11 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
147
178
|
throw failure("\u4ED3\u5E93\u72B6\u6001\u6587\u4EF6\u635F\u574F\u6216\u65E0\u6CD5\u8BFB\u53D6\uFF0C\u8BF7\u4FDD\u7559\u6587\u4EF6\u5E76\u4FEE\u590D\u540E\u91CD\u8BD5", "error.repo.state");
|
|
148
179
|
}
|
|
149
180
|
}
|
|
150
|
-
async function write(value) {
|
|
181
|
+
async function write(value, allowShrink = false) {
|
|
151
182
|
validateStoredState(value);
|
|
152
183
|
const text = JSON.stringify(value, null, 2);
|
|
153
|
-
|
|
184
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
185
|
+
if (bytes > STATE_READ_LIMIT || bytes > STATE_LIMIT && !(allowShrink && bytes < await currentStateBytes())) throw failure("\u5B89\u88C5\u6765\u6E90\u72B6\u6001\u8D85\u8FC7 16 MiB\u3002\u8BF7\u5148\u6E05\u7406\u4E0D\u9700\u8981\u7684\u56DE\u6536\u8BB0\u5F55\u6216\u65E7\u8EAB\u4EFD\uFF0C\u4ECD\u6709\u6548\u7684\u5B89\u88C5\u526F\u672C\u4F1A\u4FDD\u7559\u3002", "error.repo.capacity");
|
|
154
186
|
await fs.mkdir(managerHomePath(), { recursive: true });
|
|
155
187
|
if ((await fs.lstat(managerHomePath())).isSymbolicLink()) throw failure("\u4ED3\u5E93\u72B6\u6001\u76EE\u5F55\u4E0D\u80FD\u662F\u94FE\u63A5");
|
|
156
188
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
@@ -161,6 +193,13 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
161
193
|
await fs.rm(temporary, { force: true });
|
|
162
194
|
}
|
|
163
195
|
}
|
|
196
|
+
async function currentStateBytes() {
|
|
197
|
+
try {
|
|
198
|
+
return (await fs.lstat(file)).size;
|
|
199
|
+
} catch {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
164
203
|
function repository(data, id) {
|
|
165
204
|
const repo = data.repositories.find((r) => r.id === id);
|
|
166
205
|
if (!repo) throw failure("\u4ED3\u5E93\u4E0D\u5B58\u5728");
|
|
@@ -242,20 +281,24 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
242
281
|
const directory = await fs.lstat(receipt), file2 = await fs.lstat(join(receipt, "metadata.json"));
|
|
243
282
|
if (!directory.isDirectory() || directory.isSymbolicLink() || !file2.isFile() || file2.isSymbolicLink() || file2.size > 32 << 10) return false;
|
|
244
283
|
const metadata = JSON.parse(await fs.readFile(join(receipt, "metadata.json"), "utf8"));
|
|
245
|
-
return metadata.id === record.trashId && metadata.name === record.name && (record.workspace ? metadata.root?.scope === "project" && metadata.root.projectRoot === record.workspace : metadata.root?.scope === "user" && metadata.root.key === (record.root || "dsh")) && metadata.repository?.id === record.id && metadata.repository.path === record.path && metadata.entries?.length === 1 && metadata.entries[0] === record.name;
|
|
284
|
+
return metadata.id === record.trashId && metadata.name === record.name && (record.workspace ? metadata.root?.scope === "project" && metadata.root.projectRoot === record.workspace : metadata.root?.scope === "user" && metadata.root.key === (record.root || "dsh")) && ("path" in record ? metadata.repository?.id === record.id && metadata.repository.path === record.path : Array.isArray(metadata.catalog) && metadata.catalog.includes(record.id)) && metadata.entries?.length === 1 && metadata.entries[0] === record.name;
|
|
246
285
|
} catch {
|
|
247
286
|
return false;
|
|
248
287
|
}
|
|
249
288
|
}
|
|
250
289
|
async function recoverPending() {
|
|
290
|
+
catalogWarnings = [];
|
|
251
291
|
const data = await read();
|
|
252
292
|
let recovered = false;
|
|
293
|
+
const recoveredCatalogIds = [];
|
|
294
|
+
const recoveredInstallIds = [];
|
|
253
295
|
const restoredIds = [];
|
|
254
296
|
const registered = requestIndex().registered;
|
|
255
297
|
for (const record of data.installs.filter((record2) => !record2.complete && !record2.trashId)) {
|
|
256
298
|
if (await matches(record, signature(record.files), registered, await indexedFor(record))) {
|
|
257
299
|
record.complete = true;
|
|
258
300
|
recovered = true;
|
|
301
|
+
recoveredInstallIds.push(`${record.id}:${record.path}`);
|
|
259
302
|
}
|
|
260
303
|
}
|
|
261
304
|
for (const record of data.installs.filter((record2) => record2.trashId && record2.restoreFingerprint)) {
|
|
@@ -266,11 +309,35 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
266
309
|
recovered = true;
|
|
267
310
|
}
|
|
268
311
|
}
|
|
312
|
+
for (const record of catalogRecords(data)) {
|
|
313
|
+
if (record.complete === false && !record.trashId && record.files?.length && record.root) {
|
|
314
|
+
if (await catalogMatches(record, signature(record.files))) {
|
|
315
|
+
try {
|
|
316
|
+
const root = await recordInstallRoot(record, registered, await indexedFor(record));
|
|
317
|
+
await enableCatalog(root, record.name, true);
|
|
318
|
+
record.complete = true;
|
|
319
|
+
recovered = true;
|
|
320
|
+
recoveredCatalogIds.push(record.id);
|
|
321
|
+
} catch {
|
|
322
|
+
catalogWarnings.push({ id: record.id, code: "error.repo.installState", error: "\u6280\u80FD\u6587\u4EF6\u548C\u5B89\u88C5\u610F\u56FE\u5DF2\u4FDD\u7559\uFF0C\u542F\u7528\u7B56\u7565\u6682\u65F6\u4E0D\u53EF\u5199\uFF1B\u4FEE\u590D\u7B56\u7565\u540E\u91CD\u8BD5" });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const record of catalogRecords(data).filter((record2) => record2.root && record2.trashId && record2.restoreFingerprint)) {
|
|
328
|
+
if (await hasRestoreReceipt(record) && await catalogMatches(record, record.restoreFingerprint)) {
|
|
329
|
+
restoredIds.push(record.trashId);
|
|
330
|
+
delete record.trashId;
|
|
331
|
+
delete record.restoreFingerprint;
|
|
332
|
+
recovered = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
269
335
|
if (recovered) {
|
|
270
336
|
try {
|
|
271
|
-
await write(data);
|
|
337
|
+
await write(data, true);
|
|
272
338
|
} catch {
|
|
273
|
-
|
|
339
|
+
for (const id of [.../* @__PURE__ */ new Set([...recoveredCatalogIds, ...restoredIds, ...recoveredInstallIds])]) catalogWarnings.push({ id, code: "error.repo.installState", error: "\u6280\u80FD\u6587\u4EF6\u5DF2\u4FDD\u7559\uFF0C\u6062\u590D\u786E\u8BA4\u6682\u65F6\u65E0\u6CD5\u5199\u5165\uFF1B\u6062\u590D\u78C1\u76D8\u5199\u5165\u540E\u91CD\u8BD5" });
|
|
340
|
+
return;
|
|
274
341
|
}
|
|
275
342
|
for (const id of restoredIds) await fs.rm(join(trashRootPath(), `.restored-${id}`), { recursive: true, force: true }).catch(() => void 0);
|
|
276
343
|
}
|
|
@@ -290,38 +357,255 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
290
357
|
} catch {
|
|
291
358
|
}
|
|
292
359
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const
|
|
360
|
+
const activeCatalogIds = [];
|
|
361
|
+
const catalogCopies2 = [];
|
|
362
|
+
for (const record of catalogRecords(data).filter((record2) => record2.root && record2.complete !== false && !record2.trashId)) {
|
|
363
|
+
try {
|
|
364
|
+
const root = await recordInstallRoot(record, registered, await indexedFor(record));
|
|
365
|
+
const files = await readSkillTree(join(root.path, record.name));
|
|
366
|
+
if (files["SKILL.md"]) {
|
|
367
|
+
if (!activeCatalogIds.includes(record.id)) activeCatalogIds.push(record.id);
|
|
368
|
+
catalogCopies2.push({ id: record.id, name: record.name, installedRoot: record.root || "dsh", ...record.workspace !== void 0 ? { installedWorkspace: record.workspace } : {}, canUninstall: true });
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
installRoots: await repositoryDestinations(),
|
|
375
|
+
catalogInstalls: activeCatalogIds,
|
|
376
|
+
catalogCopies: catalogCopies2,
|
|
377
|
+
catalogUnknownIds: [...new Set((data.catalogInstalls || []).flatMap((record) => typeof record === "string" ? [record] : !record.root && !record.trashId ? [record.id] : []))].filter((id) => !activeCatalogIds.includes(id)),
|
|
378
|
+
catalogWarnings: [...catalogWarnings],
|
|
379
|
+
repositories: data.repositories.map((repo) => ({ ...repo, skills: repo.skills.map((skill) => {
|
|
380
|
+
const found = skills.some((s) => s.name.toLowerCase() === skill.name.toLowerCase() || s.declaredName?.toLowerCase() === skill.name.toLowerCase());
|
|
381
|
+
const { body, ...summary } = skill;
|
|
382
|
+
const installations = data.installs.filter((i) => i.id === repo.id && i.path === skill.path && i.name === skill.name && i.complete && !i.trashId).flatMap((record) => {
|
|
383
|
+
const tracked = present.has(record);
|
|
384
|
+
if (!tracked) return [];
|
|
385
|
+
const updateAvailable = !!skill.files && signature(record.files) !== signature(skill.files);
|
|
386
|
+
return [{
|
|
387
|
+
installedRoot: record.root || "dsh",
|
|
388
|
+
...record.workspace !== void 0 ? { installedWorkspace: record.workspace } : {},
|
|
389
|
+
tracked,
|
|
390
|
+
updateAvailable,
|
|
391
|
+
canRollback: !!record.backup,
|
|
392
|
+
canUninstall: true,
|
|
393
|
+
status: !skill.valid ? "invalid" : matched.get(record) ? updateAvailable ? "update" : "installed" : "conflict"
|
|
394
|
+
}];
|
|
395
|
+
});
|
|
396
|
+
const legacy = installations.find((item) => item.tracked);
|
|
299
397
|
return {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
398
|
+
...summary,
|
|
399
|
+
installations,
|
|
400
|
+
updateAvailable: legacy?.updateAvailable ?? false,
|
|
401
|
+
canRollback: legacy?.canRollback ?? false,
|
|
402
|
+
tracked: !!legacy,
|
|
403
|
+
installedRoot: legacy?.installedRoot ?? null,
|
|
404
|
+
installedWorkspace: legacy?.installedWorkspace ?? null,
|
|
405
|
+
canUninstall: !!legacy,
|
|
406
|
+
status: !skill.valid ? "invalid" : legacy?.status ?? (found ? "conflict" : "available")
|
|
307
407
|
};
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
408
|
+
}) }))
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function catalogRecords(data) {
|
|
412
|
+
return (data.catalogInstalls || []).filter((record) => typeof record !== "string");
|
|
413
|
+
}
|
|
414
|
+
const locationKey = (record) => JSON.stringify([
|
|
415
|
+
record.root,
|
|
416
|
+
record.workspace === void 0 ? null : process.platform === "win32" ? resolve(record.workspace).toLowerCase() : resolve(record.workspace),
|
|
417
|
+
record.name
|
|
418
|
+
]);
|
|
419
|
+
async function catalogLocation(input = {}) {
|
|
420
|
+
if (input.workspace !== void 0) {
|
|
421
|
+
if (input.root !== void 0 && input.root !== "dsh") throw failure("\u5DE5\u4F5C\u533A\u53EA\u80FD\u5B89\u88C5\u5230\u9879\u76EE DSH skills");
|
|
422
|
+
const target = await resolveWorkspaceTarget(input.workspace, requestIndex().registered);
|
|
423
|
+
if ("ok" in target && target.ok === false) throw failure(target.error, target.code, target.params);
|
|
424
|
+
return { root: "dsh", workspace: target.root.projectRoot };
|
|
425
|
+
}
|
|
426
|
+
return { root: repositoryInstallRoot(input.root).key };
|
|
427
|
+
}
|
|
428
|
+
async function enableCatalog(root, name, respectDisabled = false) {
|
|
429
|
+
try {
|
|
430
|
+
if (respectDisabled) {
|
|
431
|
+
const current = await readManagerState();
|
|
432
|
+
if (!current.writable) throw new Error("\u542F\u7528\u7B56\u7565\u4E0D\u53EF\u5199");
|
|
433
|
+
if (current.state.disabledSkills[root.key]?.includes(name)) return;
|
|
434
|
+
}
|
|
435
|
+
const result = await setSkillEnabled(root, name, true, log);
|
|
436
|
+
if (result && "ok" in result && result.ok === false) throw new Error(result.error);
|
|
437
|
+
} catch {
|
|
438
|
+
throw Object.assign(failure("\u6280\u80FD\u6587\u4EF6\u548C\u5B89\u88C5\u610F\u56FE\u5DF2\u4FDD\u7559\uFF0C\u542F\u7528\u72B6\u6001\u65E0\u6CD5\u5199\u5165\uFF1B\u6062\u590D\u78C1\u76D8\u5199\u5165\u540E\u91CD\u65B0\u6253\u5F00\u53D1\u73B0\u9875", "error.repo.installState"), { statusCode: 500 });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function catalogMatches(record, fingerprint) {
|
|
442
|
+
try {
|
|
443
|
+
const root = await recordInstallRoot(record, requestIndex().registered, await indexedFor(record));
|
|
444
|
+
return signature(fileIndex(await readSkillTree(join(root.path, record.name)))) === fingerprint;
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function catalogCopies(data, root, name) {
|
|
450
|
+
const target = { root: root.scope === "project" ? "dsh" : root.key, workspace: root.projectRoot, name };
|
|
451
|
+
return catalogRecords(data).filter((record) => record.root && !record.trashId && locationKey(record) === locationKey(target));
|
|
452
|
+
}
|
|
453
|
+
async function restoreCatalogTrash(id, options) {
|
|
454
|
+
return serialize(async () => {
|
|
455
|
+
const metadata = (await listTrash()).find((item) => item.id === id);
|
|
456
|
+
const data = await read();
|
|
457
|
+
const records = catalogRecords(data).filter((record) => record.trashId === id && metadata?.catalog?.includes(record.id));
|
|
458
|
+
if (!records.length) return restoreSkillTrash(id, log, options);
|
|
459
|
+
for (const record of records) {
|
|
460
|
+
if (metadata.name !== record.name || (record.workspace ? metadata.root?.projectRoot !== record.workspace || metadata.root.scope !== "project" : metadata.root?.scope !== "user" || metadata.root.key !== record.root)) throw failure("\u56DE\u6536\u7AD9\u6765\u6E90\u4E0E\u76EE\u5F55\u8EAB\u4EFD\u4E0D\u4E00\u81F4");
|
|
461
|
+
record.restoreFingerprint = signature(fileIndex(await readSkillTree(join(trashRootPath(), id, record.name))));
|
|
462
|
+
}
|
|
463
|
+
await write(data);
|
|
464
|
+
const result = await restoreSkillTrash(id, log, { ...options, repositoryRestore: true });
|
|
465
|
+
if ("ok" in result && result.ok === false) {
|
|
466
|
+
for (const record of records) delete record.restoreFingerprint;
|
|
467
|
+
await write(data);
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
for (const record of records) {
|
|
471
|
+
delete record.trashId;
|
|
472
|
+
delete record.restoreFingerprint;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
await write(data);
|
|
476
|
+
} catch {
|
|
477
|
+
throw failure("\u6280\u80FD\u5DF2\u6062\u590D\uFF0C\u76EE\u5F55\u8EAB\u4EFD\u7B49\u5F85\u6821\u9A8C\uFF1B\u6062\u590D\u78C1\u76D8\u5199\u5165\u540E\u91CD\u65B0\u6253\u5F00\u53D1\u73B0\u9875", "error.repo.installState");
|
|
478
|
+
}
|
|
479
|
+
await fs.rm(join(trashRootPath(), `.restored-${id}`), { recursive: true, force: true });
|
|
480
|
+
return result;
|
|
481
|
+
}, false);
|
|
322
482
|
}
|
|
483
|
+
const refreshVersions = /* @__PURE__ */ new Map();
|
|
323
484
|
return {
|
|
485
|
+
installCatalog: (id, pack, target = {}) => serialize(async () => {
|
|
486
|
+
const location = await catalogLocation(target);
|
|
487
|
+
const root = await recordInstallRoot(location, requestIndex().registered, await indexedFor(location));
|
|
488
|
+
const data = await read();
|
|
489
|
+
const previousCatalog = data.catalogInstalls;
|
|
490
|
+
let intent;
|
|
491
|
+
let reusedLegacy = false;
|
|
492
|
+
await recordInstallRoot(location, requestIndex().registered);
|
|
493
|
+
const imported = await importUploadedSkill(pack, log, {
|
|
494
|
+
conflict: "skip",
|
|
495
|
+
root,
|
|
496
|
+
beforeImport: async (pending, conflicts, failed) => {
|
|
497
|
+
const legacy = (data.catalogInstalls || []).some((record) => typeof record === "string" ? record === id : record.id === id && !record.root);
|
|
498
|
+
const candidate = pending.length === 1 && !conflicts.length ? pending[0] : !pending.length && conflicts.length === 1 && legacy ? { ...conflicts[0], isDir: conflicts[0].isDir } : void 0;
|
|
499
|
+
if (!candidate?.isDir || failed.length) throw failure("\u76EE\u5F55\u5B89\u88C5\u5FC5\u987B\u662F\u5355\u4E2A\u5B8C\u6574\u6280\u80FD\uFF0C\u4E14\u4E0D\u80FD\u8986\u76D6\u5DF2\u6709\u526F\u672C", "error.repo.conflict");
|
|
500
|
+
const files = fileIndex(await readSkillTree(candidate.source));
|
|
501
|
+
if (!files.some((file2) => file2.path === "SKILL.md")) throw failure("\u76EE\u5F55\u5B89\u88C5\u5305\u7F3A\u5C11 SKILL.md", "error.catalog.response");
|
|
502
|
+
if (conflicts.length) {
|
|
503
|
+
const local = fileIndex(await readSkillTree(join(root.path, candidate.name)));
|
|
504
|
+
if (signature(local) !== signature(files)) throw failure("\u65E7\u5B89\u88C5\u5185\u5BB9\u4E0E\u76EE\u5F55\u5305\u4E0D\u4E00\u81F4\uFF0C\u4E0D\u4F1A\u8986\u76D6\u6216\u731C\u6D4B\u8EAB\u4EFD", "error.repo.conflict");
|
|
505
|
+
reusedLegacy = true;
|
|
506
|
+
}
|
|
507
|
+
intent = { id, name: candidate.name, ...location, complete: false, files };
|
|
508
|
+
data.catalogInstalls = (data.catalogInstalls || []).filter((record) => typeof record === "string" ? record !== id : record.trashId || (record.root ? locationKey(record) !== locationKey(intent) : record.id !== id));
|
|
509
|
+
data.catalogInstalls.push(intent);
|
|
510
|
+
try {
|
|
511
|
+
await write(data);
|
|
512
|
+
} catch (caught) {
|
|
513
|
+
if (caught.code === "error.repo.capacity") throw caught;
|
|
514
|
+
throw failure("\u5B89\u88C5\u610F\u56FE\u65E0\u6CD5\u5199\u5165\uFF0C\u672A\u590D\u5236\u6280\u80FD\u6587\u4EF6", "error.repo.installState");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
const completedName = reusedLegacy ? imported.skipped?.length === 1 ? imported.skipped[0].name : void 0 : imported.imported?.length === 1 ? imported.imported[0].name : void 0;
|
|
519
|
+
if (imported.ok === false || !intent || completedName !== intent.name) {
|
|
520
|
+
if (intent) {
|
|
521
|
+
data.catalogInstalls = previousCatalog;
|
|
522
|
+
try {
|
|
523
|
+
await write(data);
|
|
524
|
+
} catch {
|
|
525
|
+
throw failure("\u5B89\u88C5\u672A\u5B8C\u6210\u4E14\u8EAB\u4EFD\u610F\u56FE\u65E0\u6CD5\u64A4\u9500\uFF0C\u8BF7\u6062\u590D\u78C1\u76D8\u5199\u5165\u540E\u91CD\u8BD5", "error.repo.installState");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
throw Object.assign(failure(imported.error || "\u540C\u540D\u6280\u80FD\u5DF2\u5B58\u5728\uFF0C\u4E0D\u4F1A\u8986\u76D6\u672C\u5730\u6587\u4EF6", imported.code || "error.repo.conflict", imported.params), { statusCode: imported.code === "error.repo.installState" ? 500 : imported.code === "error.repo.capacity" ? 413 : 409 });
|
|
529
|
+
}
|
|
530
|
+
await enableCatalog(root, intent.name);
|
|
531
|
+
intent.complete = true;
|
|
532
|
+
try {
|
|
533
|
+
await write(data);
|
|
534
|
+
} catch {
|
|
535
|
+
throw Object.assign(failure("\u6280\u80FD\u6587\u4EF6\u5DF2\u5B89\u88C5\u4F46\u8EAB\u4EFD\u672A\u786E\u8BA4\uFF1B\u6062\u590D\u78C1\u76D8\u5199\u5165\u540E\u91CD\u65B0\u6253\u5F00\u53D1\u73B0\u9875", "error.repo.installState"), { statusCode: 500 });
|
|
536
|
+
}
|
|
537
|
+
return { name: intent.name, root: root.path };
|
|
538
|
+
}),
|
|
324
539
|
list: () => serialize(list),
|
|
540
|
+
trashSkill: (root, name) => serialize(async () => {
|
|
541
|
+
const data = await read();
|
|
542
|
+
const records = root ? catalogCopies(data, root, name) : [];
|
|
543
|
+
if (!records.length) return deleteSkill(root, name, log);
|
|
544
|
+
const id = `${Date.now()}-${randomUUID()}`;
|
|
545
|
+
for (const record of records) record.trashId = id;
|
|
546
|
+
await write(data);
|
|
547
|
+
try {
|
|
548
|
+
const result = await deleteSkill(root, name, log, { catalogTrash: { id, ids: records.map((record) => record.id) } });
|
|
549
|
+
if ("ok" in result && result.ok === false) {
|
|
550
|
+
for (const record of records) delete record.trashId;
|
|
551
|
+
await write(data);
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (!(await listTrash()).some((item) => item.id === id)) {
|
|
556
|
+
for (const record of records) delete record.trashId;
|
|
557
|
+
await write(data);
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}, false),
|
|
562
|
+
permanentlyDeleteTrash: (id) => serialize(async () => {
|
|
563
|
+
const data = await read();
|
|
564
|
+
const before = data.catalogInstalls || [];
|
|
565
|
+
const next = before.filter((record) => typeof record === "string" || record.trashId !== id);
|
|
566
|
+
if (next.length !== before.length) {
|
|
567
|
+
data.catalogInstalls = next;
|
|
568
|
+
await write(data, true);
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const result = await permanentlyDeleteTrash(id, log);
|
|
572
|
+
if ("ok" in result && result.ok === false) {
|
|
573
|
+
data.catalogInstalls = before;
|
|
574
|
+
if (next.length !== before.length) await write(data, true);
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
data.catalogInstalls = before;
|
|
580
|
+
if (next.length !== before.length) await write(data, true);
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
}, false),
|
|
584
|
+
rememberCatalogInstall: (id, name, target = {}) => serialize(async () => {
|
|
585
|
+
const location = await catalogLocation(target);
|
|
586
|
+
const record = { id, name, ...location };
|
|
587
|
+
const data = await read();
|
|
588
|
+
data.catalogInstalls = (data.catalogInstalls || []).filter((item) => typeof item === "string" || item.trashId || item.id !== id || locationKey(item) !== locationKey(record));
|
|
589
|
+
data.catalogInstalls.push(record);
|
|
590
|
+
await write(data);
|
|
591
|
+
return { catalogInstalls: catalogRecords(data).map((record2) => record2.id) };
|
|
592
|
+
}, false),
|
|
593
|
+
forgetCatalogInstall: (target) => serialize(async () => {
|
|
594
|
+
if (!target || typeof target !== "object") throw failure("\u6E05\u7406\u76EE\u5F55\u8EAB\u4EFD\u5FC5\u987B\u6307\u5B9A\u5B89\u88C5\u526F\u672C\u6216\u65E7\u8EAB\u4EFD ID");
|
|
595
|
+
if ("legacyId" in target) {
|
|
596
|
+
if (typeof target.legacyId !== "string" || !/^(?:clawhub|skillsh):[A-Za-z0-9][A-Za-z0-9._-]{0,127}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}){1,2}$/.test(target.legacyId)) throw failure("\u6E05\u7406\u65E7\u76EE\u5F55\u8EAB\u4EFD\u5FC5\u987B\u6307\u5B9A\u6709\u6548 ID");
|
|
597
|
+
const data2 = await read();
|
|
598
|
+
data2.catalogInstalls = (data2.catalogInstalls || []).filter((record) => typeof record === "string" ? record !== target.legacyId : record.root || record.trashId || record.id !== target.legacyId);
|
|
599
|
+
await write(data2, true);
|
|
600
|
+
return { catalogInstalls: catalogRecords(data2).map((record) => record.id) };
|
|
601
|
+
}
|
|
602
|
+
if (typeof target.name !== "string") throw failure("\u6E05\u7406\u76EE\u5F55\u8EAB\u4EFD\u5FC5\u987B\u6307\u5B9A\u5B89\u88C5\u526F\u672C");
|
|
603
|
+
const location = await catalogLocation(target);
|
|
604
|
+
const data = await read();
|
|
605
|
+
data.catalogInstalls = (data.catalogInstalls || []).filter((record) => typeof record === "string" || record.trashId || !record.root || locationKey(record) !== locationKey({ ...location, name: target.name }) || target.id !== void 0 && record.id !== target.id);
|
|
606
|
+
await write(data, true);
|
|
607
|
+
return { catalogInstalls: catalogRecords(data).map((record) => record.id) };
|
|
608
|
+
}, false),
|
|
325
609
|
...createRepositoryUpdater({ read, write, repository, download, serialize, parseRepositoryInput, registeredWorkspaces, onWorkspaceIndexBuild }),
|
|
326
610
|
add: (input) => serialize(async () => {
|
|
327
611
|
const source = parseRepositoryInput(input), data = await read();
|
|
@@ -339,14 +623,22 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
339
623
|
for (const record of data.installs.filter((i) => i.id === id)) record.source ||= { owner: repo.owner, name: repo.name, ref: repo.ref, subdirectory: repo.subdirectory };
|
|
340
624
|
data.repositories = data.repositories.filter((r) => r.id !== id);
|
|
341
625
|
await write(data);
|
|
626
|
+
refreshVersions.set(id, (refreshVersions.get(id) || 0) + 1);
|
|
342
627
|
return { id };
|
|
343
628
|
}, false),
|
|
344
|
-
refresh: ({ id }) =>
|
|
345
|
-
const
|
|
629
|
+
refresh: async ({ id }) => {
|
|
630
|
+
const { repo, version } = await serialize(async () => {
|
|
631
|
+
const repo2 = { ...repository(await read(), id) };
|
|
632
|
+
const version2 = (refreshVersions.get(id) || 0) + 1;
|
|
633
|
+
refreshVersions.set(id, version2);
|
|
634
|
+
return { repo: repo2, version: version2 };
|
|
635
|
+
}, false);
|
|
636
|
+
let bytes;
|
|
637
|
+
let skills = [];
|
|
638
|
+
let scanError = null;
|
|
346
639
|
try {
|
|
347
|
-
|
|
640
|
+
bytes = await fetchBranch(repo);
|
|
348
641
|
const entries = decodeRepositoryArchive(bytes);
|
|
349
|
-
const skills = [];
|
|
350
642
|
for (const [path, bytes2] of Object.entries(entries)) {
|
|
351
643
|
if (path !== "SKILL.md" && !path.endsWith("/SKILL.md")) continue;
|
|
352
644
|
const directory = path === "SKILL.md" ? "" : path.slice(0, -9);
|
|
@@ -360,24 +652,32 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
360
652
|
if (files.length > 1e3) throw failure("\u6280\u80FD\u6587\u4EF6\u6570\u91CF\u8D85\u9650");
|
|
361
653
|
skills.push({ path: directory, files, name: name || directory.split("/").pop() || repo.name, description, body: parsed.body.trim(), documentHash: hash(bytes2), valid: parsed.hasFrontmatter && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(name) && name.length <= 100 && !!description });
|
|
362
654
|
}
|
|
363
|
-
const snapshot = await cacheArchive(repo, bytes);
|
|
364
|
-
Object.assign(repo, { skills, commit: snapshot, refreshedAt: (/* @__PURE__ */ new Date()).toISOString(), error: null });
|
|
365
655
|
} catch (caught) {
|
|
366
656
|
const error = caught;
|
|
367
|
-
|
|
657
|
+
scanError = { code: error.code || "error.repo.invalid", error: error.message };
|
|
368
658
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
659
|
+
return serialize(async () => {
|
|
660
|
+
const data = await read(), current = repository(data, id);
|
|
661
|
+
if (refreshVersions.get(id) !== version) return current;
|
|
662
|
+
if (["owner", "name", "ref", "subdirectory"].some((key) => current[key] !== repo[key])) throw failure("\u4ED3\u5E93\u6765\u6E90\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u5237\u65B0");
|
|
663
|
+
if (scanError) current.error = scanError;
|
|
664
|
+
else {
|
|
665
|
+
const snapshot = await cacheArchive(current, bytes);
|
|
666
|
+
Object.assign(current, { skills, commit: snapshot, refreshedAt: (/* @__PURE__ */ new Date()).toISOString(), error: null });
|
|
377
667
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
668
|
+
await write(data);
|
|
669
|
+
if (!current.error) {
|
|
670
|
+
try {
|
|
671
|
+
await pruneCache(current);
|
|
672
|
+
} catch (caught) {
|
|
673
|
+
const error = caught;
|
|
674
|
+
if (log) await log("repository.cache.cleanup.failed", { repository: current.id, error: error.message });
|
|
675
|
+
else console.warn("\u4ED3\u5E93\u65E7\u7F13\u5B58\u672A\u6E05\u7406\uFF1A" + error.message);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return current;
|
|
679
|
+
}, false);
|
|
680
|
+
},
|
|
381
681
|
detail: async ({ id, path }) => {
|
|
382
682
|
const repo = repository(await read(), id), skill = repo.skills.find((s) => s.path === path);
|
|
383
683
|
if (!skill) throw failure("\u6280\u80FD\u4E0D\u5728\u4ED3\u5E93\u5217\u8868\u4E2D");
|
|
@@ -385,6 +685,7 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
385
685
|
},
|
|
386
686
|
restoreTrash: async (id, options = {}) => {
|
|
387
687
|
const metadata = (await listTrash()).find((item) => item.id === id);
|
|
688
|
+
if (metadata?.catalog?.length && !metadata.repository) return restoreCatalogTrash(id, options);
|
|
388
689
|
if (!metadata?.repository) return restoreSkillTrash(id, log, options);
|
|
389
690
|
return serialize(async () => {
|
|
390
691
|
const metadata2 = (await listTrash()).find((item) => item.id === id);
|
|
@@ -394,15 +695,22 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
394
695
|
if (!record) return restoreSkillTrash(id, log, options);
|
|
395
696
|
if (metadata2.name !== record.name || (record.workspace ? metadata2.root?.scope !== "project" || metadata2.root.projectRoot !== record.workspace : metadata2.root?.scope !== "user" || metadata2.root.key !== (record.root || "dsh")) || metadata2.entries.length !== 1 || metadata2.entries[0] !== record.name) throw failure("\u56DE\u6536\u7AD9\u6765\u6E90\u4E0E\u5B89\u88C5\u8BB0\u5F55\u4E0D\u4E00\u81F4");
|
|
396
697
|
record.restoreFingerprint = signature(fileIndex(await readSkillTree(join(trashRootPath(), id, record.name))));
|
|
698
|
+
const catalog = catalogRecords(data).filter((item) => item.trashId === id && metadata2.catalog?.includes(item.id));
|
|
699
|
+
for (const item of catalog) item.restoreFingerprint = record.restoreFingerprint;
|
|
397
700
|
await write(data);
|
|
398
701
|
const result = await restoreSkillTrash(id, log, { ...options, repositoryRestore: true });
|
|
399
702
|
if ("ok" in result && result.ok === false) {
|
|
400
703
|
delete record.restoreFingerprint;
|
|
704
|
+
for (const item of catalog) delete item.restoreFingerprint;
|
|
401
705
|
await write(data);
|
|
402
706
|
return result;
|
|
403
707
|
}
|
|
404
708
|
delete record.trashId;
|
|
405
709
|
delete record.restoreFingerprint;
|
|
710
|
+
for (const item of catalog) {
|
|
711
|
+
delete item.trashId;
|
|
712
|
+
delete item.restoreFingerprint;
|
|
713
|
+
}
|
|
406
714
|
try {
|
|
407
715
|
await write(data);
|
|
408
716
|
} catch {
|
|
@@ -421,14 +729,18 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
421
729
|
const fingerprint = signature(fileIndex(await readSkillTree(join(root.path, record.name))));
|
|
422
730
|
const trashId = `${Date.now()}-${randomUUID()}`;
|
|
423
731
|
record.trashId = trashId;
|
|
732
|
+
const catalog = catalogCopies(data, root, record.name);
|
|
733
|
+
for (const item of catalog) item.trashId = trashId;
|
|
424
734
|
await write(data);
|
|
425
735
|
try {
|
|
426
|
-
|
|
736
|
+
await recordInstallRoot(record, index.registered, await indexedFor(record));
|
|
737
|
+
const result = await deleteSkill(root, record.name, log, { repositoryEntryOnly: true, repositoryTrash: { id: trashId, repository: { id, path } }, ...catalog.length ? { catalogTrash: { id: trashId, ids: catalog.map((item) => item.id) } } : {} });
|
|
427
738
|
if ("ok" in result && result.ok === false) throw failure(result.error, typeof result.code === "string" ? result.code : "error.repo.invalid", result.params);
|
|
428
739
|
return result;
|
|
429
740
|
} catch (error) {
|
|
430
741
|
if (await matches(record, fingerprint)) {
|
|
431
742
|
delete record.trashId;
|
|
743
|
+
for (const item of catalog) delete item.trashId;
|
|
432
744
|
await write(data);
|
|
433
745
|
}
|
|
434
746
|
throw error;
|
|
@@ -442,7 +754,7 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
442
754
|
if ("ok" in target && target.ok === false) throw failure(target.error, target.code, target.params);
|
|
443
755
|
root = target.root;
|
|
444
756
|
workspaceRoot = root.projectRoot;
|
|
445
|
-
} else root =
|
|
757
|
+
} else root = await recordInstallRoot({ root: requestedRoot });
|
|
446
758
|
const data = await read(), repo = repository(data, id);
|
|
447
759
|
const skill = repo.skills.find((s) => s.path === path);
|
|
448
760
|
if (!skill || !skill.valid) throw failure("\u6280\u80FD\u4E0D\u5B58\u5728\u6216\u683C\u5F0F\u65E0\u6548");
|
|
@@ -460,7 +772,8 @@ function createRepositoryManager({ fetchImpl = globalThis.fetch, log, registered
|
|
|
460
772
|
await write(data);
|
|
461
773
|
let copied = false;
|
|
462
774
|
try {
|
|
463
|
-
|
|
775
|
+
await recordInstallRoot({ root: requestedRoot, workspace: workspaceRoot }, registered);
|
|
776
|
+
const result = await importUploadedSkill({ name: skill.name, entries }, log, { conflict: "skip", root });
|
|
464
777
|
if (result.ok === false || !result.imported?.length) throw failure(result.error || "\u6280\u80FD\u5B89\u88C5\u5931\u8D25\u6216\u9047\u5230\u540C\u540D\u51B2\u7A81", typeof result.code === "string" ? result.code : "error.repo.conflict", result.params);
|
|
465
778
|
copied = true;
|
|
466
779
|
const enabled = await setSkillEnabled(root, skill.name, true, log);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { join, dirname, resolve } from "node:path";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { managerHomePath, userRoots, projectRootForRecord, resolveWorkspaceTarget, createWorkspaceIndex } from "./core.js";
|
|
4
|
+
import { managerHomePath, userRoots, checkedRepositoryRootDefinition, projectRootForRecord, resolveWorkspaceTarget, createWorkspaceIndex } from "./core.js";
|
|
5
5
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
6
6
|
const repositoryInstallKeys = /* @__PURE__ */ new Set(["dsh", "agents"]);
|
|
7
7
|
function repositoryInstallRoot(key = "dsh") {
|
|
@@ -12,7 +12,13 @@ function repositoryInstallRoot(key = "dsh") {
|
|
|
12
12
|
return { ...root, key: root.key };
|
|
13
13
|
}
|
|
14
14
|
async function recordInstallRoot(record, registered, indexed) {
|
|
15
|
-
if (record.workspace === void 0)
|
|
15
|
+
if (record.workspace === void 0) {
|
|
16
|
+
const root = repositoryInstallRoot(record.root);
|
|
17
|
+
const checked = await checkedRepositoryRootDefinition(root);
|
|
18
|
+
if (!checked) throw failure("\u6280\u80FD\u5B89\u88C5\u4F4D\u7F6E\u4E0D\u53EF\u7528", "error.root.unsafe");
|
|
19
|
+
if (checked.ok === false) throw Object.assign(failure(checked.error, checked.code || "error.root.unsafe"), { params: checked.params });
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
16
22
|
const resolved = await projectRootForRecord(record.workspace, registered, indexed);
|
|
17
23
|
if ("ok" in resolved && resolved.ok === false) throw Object.assign(failure(resolved.error, resolved.code || "error.workspace.unavailable"), { params: resolved.params });
|
|
18
24
|
return resolved.root;
|