dsh-skill-hub 0.3.6 → 0.3.7
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/lib/client.js +890 -356
- package/lib/client.js.map +1 -1
- package/lib/index.js +401 -47
- package/lib/types/client/SkillHubSettingsCard.d.ts +0 -2
- package/lib/types/client/api.d.ts +5 -1
- package/lib/types/client/helpers.d.ts +10 -0
- package/lib/types/client/locales.d.ts +30 -0
- package/lib/types/client/panel/DisabledRow.d.ts +2 -0
- package/lib/types/client/panel/SkillDetailView.d.ts +5 -1
- package/lib/types/client/panel/dialogs.d.ts +19 -0
- package/lib/types/client/panel/useSkillHub.d.ts +12 -1
- package/lib/types/client/settings-card.d.ts +0 -17
- package/lib/types/client/settings-form.d.ts +0 -1
- package/lib/types/protocol.d.ts +40 -17
- package/lib/types/repo.d.ts +4 -6
- package/lib/types/skillfs.d.ts +13 -0
- package/package.json +29 -28
- package/src/client/SkillHubSettingsCard.tsx +0 -2
- package/src/client/api.ts +18 -0
- package/src/client/helpers.ts +40 -0
- package/src/client/locales.ts +60 -0
- package/src/client/panel/DisabledRow.tsx +22 -7
- package/src/client/panel/MarketView.tsx +8 -1
- package/src/client/panel/ScenesView.tsx +1 -1
- package/src/client/panel/SkillDetailView.tsx +35 -11
- package/src/client/panel/SkillHubPanel.tsx +42 -4
- package/src/client/panel/SkillRow.tsx +5 -2
- package/src/client/panel/SourcesView.tsx +2 -17
- package/src/client/panel/dialogs.tsx +61 -0
- package/src/client/panel/panel.module.css +1 -8
- package/src/client/panel/useSkillHub.ts +97 -12
- package/src/client/settings-card.tsx +0 -42
- package/src/client/settings-form.ts +1 -5
- package/src/protocol.ts +42 -8
- package/src/repo.ts +71 -29
- package/src/routes.test.ts +22 -0
- package/src/routes.ts +181 -10
- package/src/skillfs.ts +188 -3
package/lib/index.js
CHANGED
|
@@ -46,7 +46,9 @@ const SKILL_HUB_API = {
|
|
|
46
46
|
sourceSync: "/api/skill-hub/sources/sync",
|
|
47
47
|
sourceDelete: "/api/skill-hub/sources/delete",
|
|
48
48
|
sourceRestore: "/api/skill-hub/sources/restore",
|
|
49
|
-
sourceTrashClear: "/api/skill-hub/sources/trash/clear"
|
|
49
|
+
sourceTrashClear: "/api/skill-hub/sources/trash/clear",
|
|
50
|
+
diagnosticFix: "/api/skill-hub/diagnostic/fix",
|
|
51
|
+
marketSourceVersions: "/api/skill-hub/market/source/versions"
|
|
50
52
|
};
|
|
51
53
|
/**
|
|
52
54
|
* Hub config defaults — the single source every layer reads: the cordis
|
|
@@ -867,10 +869,18 @@ function parseFrontmatter(text) {
|
|
|
867
869
|
const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n([\s\S]*))?$/.exec(text);
|
|
868
870
|
if (match === null) return { error: "missing YAML frontmatter (--- block)" };
|
|
869
871
|
let data;
|
|
872
|
+
let rawFrontmatter = match[1];
|
|
870
873
|
try {
|
|
871
|
-
data = load(
|
|
874
|
+
data = load(rawFrontmatter);
|
|
872
875
|
} catch (error) {
|
|
873
|
-
|
|
876
|
+
const repaired = repairFrontmatterScalarFields(rawFrontmatter);
|
|
877
|
+
if (repaired !== null) try {
|
|
878
|
+
data = load(repaired);
|
|
879
|
+
rawFrontmatter = repaired;
|
|
880
|
+
} catch {
|
|
881
|
+
return { error: "invalid YAML frontmatter: " + (error instanceof Error ? error.message : String(error)) };
|
|
882
|
+
}
|
|
883
|
+
else return { error: "invalid YAML frontmatter: " + (error instanceof Error ? error.message : String(error)) };
|
|
874
884
|
}
|
|
875
885
|
if (typeof data !== "object" || data === null || Array.isArray(data)) return { error: "frontmatter must be a YAML mapping" };
|
|
876
886
|
const record = data;
|
|
@@ -902,6 +912,111 @@ function parseFrontmatter(text) {
|
|
|
902
912
|
content: (match[2] ?? "").trim()
|
|
903
913
|
} };
|
|
904
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Repair bare scalar fields that contain an unquoted colon (e.g. `description: Build for AWS: ECS`)
|
|
917
|
+
* or an invalid flow-like scalar (`@`, `` ` ``, `[`, `{`). Mirrors codex `repair_frontmatter_scalar_fields`:
|
|
918
|
+
* line-oriented, only touches lines where quoting makes the YAML valid, and preserves block scalars.
|
|
919
|
+
* Returns the repaired frontmatter string when at least one line was quoted, otherwise null.
|
|
920
|
+
*/
|
|
921
|
+
function repairFrontmatterScalarFields(frontmatter) {
|
|
922
|
+
let changed = false;
|
|
923
|
+
let blockScalarIndent = null;
|
|
924
|
+
const repairedLines = [];
|
|
925
|
+
for (const line of frontmatter.split("\n")) {
|
|
926
|
+
const indent = line.search(/[^ ]/);
|
|
927
|
+
const effectiveIndent = indent === -1 ? line.length : indent;
|
|
928
|
+
if (blockScalarIndent !== null) {
|
|
929
|
+
if (line.trim() === "" || effectiveIndent > blockScalarIndent) {
|
|
930
|
+
repairedLines.push(line);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
blockScalarIndent = null;
|
|
934
|
+
}
|
|
935
|
+
const colonIndex = line.indexOf(":");
|
|
936
|
+
if (colonIndex === -1) {
|
|
937
|
+
repairedLines.push(line);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const key = line.slice(0, colonIndex);
|
|
941
|
+
const value = line.slice(colonIndex + 1);
|
|
942
|
+
if (key.trim() === "" || value.length === 0 || !/^\s/.test(value)) {
|
|
943
|
+
repairedLines.push(line);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const trimmedStart = value.trimStart();
|
|
947
|
+
const leadingWhitespace = value.slice(0, value.length - trimmedStart.length);
|
|
948
|
+
let scalar = trimmedStart;
|
|
949
|
+
let comment = "";
|
|
950
|
+
for (let idx = 0; idx < trimmedStart.length; idx += 1) if (trimmedStart[idx] === "#") {
|
|
951
|
+
const prev = idx === 0 ? " " : trimmedStart[idx - 1];
|
|
952
|
+
const next = idx + 1 < trimmedStart.length ? trimmedStart[idx + 1] : " ";
|
|
953
|
+
if (/\s/.test(prev) && /\s/.test(next)) {
|
|
954
|
+
const commentStart = trimmedStart.slice(0, idx).trimEnd().length;
|
|
955
|
+
scalar = trimmedStart.slice(0, commentStart);
|
|
956
|
+
comment = trimmedStart.slice(commentStart);
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
scalar = scalar.trimEnd();
|
|
961
|
+
if (scalar === "") {
|
|
962
|
+
repairedLines.push(line);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const firstChar = scalar[0];
|
|
966
|
+
if (firstChar === "|" || firstChar === ">") {
|
|
967
|
+
blockScalarIndent = effectiveIndent;
|
|
968
|
+
repairedLines.push(line);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (firstChar === "'" || firstChar === "\"") {
|
|
972
|
+
repairedLines.push(line);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
let hasColonSeparator = false;
|
|
976
|
+
for (let i = 0; i < scalar.length - 1; i += 1) if (scalar[i] === ":" && /\s/.test(scalar[i + 1])) {
|
|
977
|
+
hasColonSeparator = true;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
let invalidFlowLike = false;
|
|
981
|
+
if (firstChar === "[" || firstChar === "{" || firstChar === "@" || firstChar === "`") try {
|
|
982
|
+
load(scalar);
|
|
983
|
+
} catch {
|
|
984
|
+
invalidFlowLike = true;
|
|
985
|
+
}
|
|
986
|
+
if (!hasColonSeparator && !invalidFlowLike) {
|
|
987
|
+
repairedLines.push(line);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const quotedScalar = "'" + scalar.replace(/'/g, "''") + "'";
|
|
991
|
+
repairedLines.push(key + ":" + leadingWhitespace + quotedScalar + comment);
|
|
992
|
+
changed = true;
|
|
993
|
+
}
|
|
994
|
+
return changed ? repairedLines.join("\n") : null;
|
|
995
|
+
}
|
|
996
|
+
/** Try to repair a SKILL.md's frontmatter in place (unquoted colon etc.). Returns the new file text or null when not fixable. */
|
|
997
|
+
function repairFrontmatterFileText(text) {
|
|
998
|
+
const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n([\s\S]*))?$/.exec(text);
|
|
999
|
+
if (match === null) return null;
|
|
1000
|
+
const repaired = repairFrontmatterScalarFields(match[1]);
|
|
1001
|
+
if (repaired === null) return null;
|
|
1002
|
+
try {
|
|
1003
|
+
load(repaired);
|
|
1004
|
+
} catch {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
const body = match[2] ?? "";
|
|
1008
|
+
return "---\n" + repaired + "\n---" + (body !== "" ? "\n" + body : "");
|
|
1009
|
+
}
|
|
1010
|
+
/** Repair one file on disk when its frontmatter is auto-fixable. Returns the new text. */
|
|
1011
|
+
async function fixDiagnosticFile(path, home = dshHome()) {
|
|
1012
|
+
if (rootOfPath(path, home) === void 0) throw new TypeError("not a hub writable skill path: " + path);
|
|
1013
|
+
const repairedText = repairFrontmatterFileText(await readFile(path, "utf8"));
|
|
1014
|
+
if (repairedText === null) throw new TypeError("diagnostic is not auto-fixable: " + path);
|
|
1015
|
+
const parsed = parseFrontmatter(repairedText);
|
|
1016
|
+
if ("error" in parsed) throw new TypeError("repaired frontmatter still invalid: " + parsed.error);
|
|
1017
|
+
await writeFile(path, repairedText, "utf8");
|
|
1018
|
+
return path;
|
|
1019
|
+
}
|
|
905
1020
|
/** Official boolean grammar: true/false, 1/0, and the common string spellings. */
|
|
906
1021
|
function frontmatterBoolean(data, key) {
|
|
907
1022
|
if (!Object.hasOwn(data, key)) return void 0;
|
|
@@ -959,6 +1074,59 @@ async function scanRoot(base) {
|
|
|
959
1074
|
function listSkillEntries(root, home = dshHome()) {
|
|
960
1075
|
return scanRoot(rootPath(root, home));
|
|
961
1076
|
}
|
|
1077
|
+
/** Read UI metadata from `agents/openai.yaml` beside a directory skill (mirrors codex SkillInterface). */
|
|
1078
|
+
async function readSkillInterface(directory) {
|
|
1079
|
+
const yamlPath = join(directory, "agents", "openai.yaml");
|
|
1080
|
+
let text;
|
|
1081
|
+
try {
|
|
1082
|
+
text = await readFile(yamlPath, "utf8");
|
|
1083
|
+
} catch {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
let data;
|
|
1087
|
+
try {
|
|
1088
|
+
data = load(text);
|
|
1089
|
+
} catch {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
|
|
1093
|
+
const record = data;
|
|
1094
|
+
const iface = record.interface ?? record;
|
|
1095
|
+
const ifaceObj = typeof record.interface === "object" && record.interface !== null && !Array.isArray(record.interface) ? record.interface : iface;
|
|
1096
|
+
const cleanStr = (value, maxLen) => {
|
|
1097
|
+
if (typeof value !== "string") return void 0;
|
|
1098
|
+
const cleaned = value.split(/\s+/).join(" ").trim();
|
|
1099
|
+
if (cleaned === "" || cleaned.length > maxLen) return void 0;
|
|
1100
|
+
return cleaned;
|
|
1101
|
+
};
|
|
1102
|
+
const hexColor = (value) => {
|
|
1103
|
+
if (typeof value !== "string") return void 0;
|
|
1104
|
+
const trimmed = value.trim();
|
|
1105
|
+
return /^#[0-9a-fA-F]{6}$/.test(trimmed) ? trimmed : void 0;
|
|
1106
|
+
};
|
|
1107
|
+
const iconPath = (value) => {
|
|
1108
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
1109
|
+
const trimmed = value.trim().replace(/\\/g, "/");
|
|
1110
|
+
if (trimmed.startsWith("/") || trimmed.includes("..")) return void 0;
|
|
1111
|
+
if (!trimmed.startsWith("assets/")) return void 0;
|
|
1112
|
+
return trimmed;
|
|
1113
|
+
};
|
|
1114
|
+
const displayName = cleanStr(ifaceObj.display_name, 64);
|
|
1115
|
+
const shortDescription = cleanStr(ifaceObj.short_description, 1024);
|
|
1116
|
+
const brandColor = hexColor(ifaceObj.brand_color);
|
|
1117
|
+
const defaultPrompt = cleanStr(ifaceObj.default_prompt, 1024);
|
|
1118
|
+
const iconSmall = iconPath(ifaceObj.icon_small);
|
|
1119
|
+
const iconLarge = iconPath(ifaceObj.icon_large);
|
|
1120
|
+
if (!(displayName !== void 0 || shortDescription !== void 0 || brandColor !== void 0 || iconSmall !== void 0 || iconLarge !== void 0 || defaultPrompt !== void 0)) return void 0;
|
|
1121
|
+
return {
|
|
1122
|
+
...displayName !== void 0 ? { displayName } : {},
|
|
1123
|
+
...shortDescription !== void 0 ? { shortDescription } : {},
|
|
1124
|
+
...brandColor !== void 0 ? { brandColor } : {},
|
|
1125
|
+
...iconSmall !== void 0 ? { iconSmall } : {},
|
|
1126
|
+
...iconLarge !== void 0 ? { iconLarge } : {},
|
|
1127
|
+
...defaultPrompt !== void 0 ? { defaultPrompt } : {}
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
962
1130
|
/**
|
|
963
1131
|
* Walk up from cwd (max 32 levels) for a project marker (.dsh or .git);
|
|
964
1132
|
* falls back to cwd itself. The provider roots project skills here.
|
|
@@ -995,13 +1163,21 @@ async function scanDiagnostics(root, home = dshHome()) {
|
|
|
995
1163
|
}
|
|
996
1164
|
const parsed = parseFrontmatter(text);
|
|
997
1165
|
if ("error" in parsed) {
|
|
1166
|
+
const fixable = repairFrontmatterFileText(text) !== null;
|
|
998
1167
|
diagnostics.push({
|
|
999
1168
|
path: entry.path,
|
|
1000
1169
|
root,
|
|
1001
|
-
reason: parsed.error
|
|
1170
|
+
reason: parsed.error,
|
|
1171
|
+
...fixable ? { fixable: true } : {}
|
|
1002
1172
|
});
|
|
1003
1173
|
continue;
|
|
1004
1174
|
}
|
|
1175
|
+
if (repairFrontmatterFileText(text) !== null) diagnostics.push({
|
|
1176
|
+
path: entry.path,
|
|
1177
|
+
root,
|
|
1178
|
+
reason: "frontmatter contains unquoted colon/bracket (auto-repaired in memory; click Fix to persist)",
|
|
1179
|
+
fixable: true
|
|
1180
|
+
});
|
|
1005
1181
|
const { value } = parsed;
|
|
1006
1182
|
const pathName = entry.kind === "directory" ? basename(entry.directory) : basename(entry.path, ".md");
|
|
1007
1183
|
if (value.name !== pathName) diagnostics.push({
|
|
@@ -1203,6 +1379,22 @@ let githubToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? "";
|
|
|
1203
1379
|
function githubAuthHeaders() {
|
|
1204
1380
|
return githubToken === "" ? {} : { authorization: "Bearer " + githubToken };
|
|
1205
1381
|
}
|
|
1382
|
+
/** ETag cache for GitHub API JSON (mirrors codex lib.rs marker+fingerprint). In-memory only, saves 304 for daily checks. */
|
|
1383
|
+
const etagCache = /* @__PURE__ */ new Map();
|
|
1384
|
+
const ETAG_MAX = 200;
|
|
1385
|
+
function etagCacheGet(url) {
|
|
1386
|
+
return etagCache.get(url);
|
|
1387
|
+
}
|
|
1388
|
+
function etagCacheSet(url, etag, json) {
|
|
1389
|
+
if (etagCache.size >= ETAG_MAX) {
|
|
1390
|
+
const first = etagCache.keys().next().value;
|
|
1391
|
+
if (first !== void 0) etagCache.delete(first);
|
|
1392
|
+
}
|
|
1393
|
+
etagCache.set(url, {
|
|
1394
|
+
etag,
|
|
1395
|
+
json
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1206
1398
|
/**
|
|
1207
1399
|
* Build a RepoFetchError; when the response shows an exhausted rate limit,
|
|
1208
1400
|
* report the reset time instead of a bare HTTP status.
|
|
@@ -1257,47 +1449,50 @@ function normalizeRepoInput(input) {
|
|
|
1257
1449
|
* tree is fetched at `ref ?? default_branch` so an explicit branch/tag scans
|
|
1258
1450
|
* the exact same content a later download would fetch.
|
|
1259
1451
|
*/
|
|
1260
|
-
async function
|
|
1261
|
-
|
|
1452
|
+
async function fetchJsonCached(url, fetchImpl, context) {
|
|
1453
|
+
const cached = etagCacheGet(url);
|
|
1454
|
+
const headers = {
|
|
1455
|
+
accept: "application/vnd.github+json",
|
|
1456
|
+
...githubAuthHeaders()
|
|
1457
|
+
};
|
|
1458
|
+
if (cached !== void 0) headers["if-none-match"] = cached.etag;
|
|
1459
|
+
let response;
|
|
1262
1460
|
try {
|
|
1263
|
-
|
|
1264
|
-
accept: "application/vnd.github+json",
|
|
1265
|
-
...githubAuthHeaders()
|
|
1266
|
-
} });
|
|
1461
|
+
response = await fetchImpl(url, { headers });
|
|
1267
1462
|
} catch (error) {
|
|
1268
|
-
throw new RepoFetchError(
|
|
1463
|
+
throw new RepoFetchError(context + ": " + (error instanceof Error ? error.message : String(error)));
|
|
1269
1464
|
}
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1465
|
+
if (response.status === 304 && cached !== void 0) return {
|
|
1466
|
+
json: cached.json,
|
|
1467
|
+
response
|
|
1468
|
+
};
|
|
1469
|
+
if (!response.ok) throw fetchError(context, response);
|
|
1470
|
+
let json;
|
|
1272
1471
|
try {
|
|
1273
|
-
|
|
1472
|
+
json = await response.json();
|
|
1274
1473
|
} catch {
|
|
1275
|
-
throw new RepoFetchError("invalid github
|
|
1474
|
+
throw new RepoFetchError("invalid github response for " + url);
|
|
1276
1475
|
}
|
|
1476
|
+
const etag = response.headers.get("etag");
|
|
1477
|
+
if (etag !== null && etag !== "") etagCacheSet(url, etag, json);
|
|
1478
|
+
return {
|
|
1479
|
+
json,
|
|
1480
|
+
response
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async function loadRepoTree(repo, ref, fetchImpl = fetch) {
|
|
1484
|
+
const { json: metaJson } = await fetchJsonCached(`https://api.github.com/repos/${repo}`, fetchImpl, "github repo not found or unavailable");
|
|
1485
|
+
let meta = metaJson;
|
|
1277
1486
|
const record = typeof meta === "object" && meta !== null ? meta : {};
|
|
1278
1487
|
const defaultBranch = typeof record.default_branch === "string" && record.default_branch !== "" ? record.default_branch : "main";
|
|
1279
1488
|
const treeRef = ref !== void 0 && ref !== "" ? ref : defaultBranch;
|
|
1280
|
-
|
|
1281
|
-
try {
|
|
1282
|
-
treeResponse = await fetchImpl(`https://api.github.com/repos/${repo}/git/trees/${encodeURIComponent(treeRef)}?recursive=1`, { headers: {
|
|
1283
|
-
accept: "application/vnd.github+json",
|
|
1284
|
-
...githubAuthHeaders()
|
|
1285
|
-
} });
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
throw new RepoFetchError("github tree request failed: " + (error instanceof Error ? error.message : String(error)));
|
|
1288
|
-
}
|
|
1289
|
-
if (!treeResponse.ok) throw fetchError("github tree not found", treeResponse);
|
|
1290
|
-
let payload;
|
|
1291
|
-
try {
|
|
1292
|
-
payload = await treeResponse.json();
|
|
1293
|
-
} catch {
|
|
1294
|
-
throw new RepoFetchError("invalid github tree response");
|
|
1295
|
-
}
|
|
1489
|
+
const { json: payload } = await fetchJsonCached(`https://api.github.com/repos/${repo}/git/trees/${encodeURIComponent(treeRef)}?recursive=1`, fetchImpl, "github tree not found");
|
|
1296
1490
|
const treeRecord = typeof payload === "object" && payload !== null ? payload : {};
|
|
1297
|
-
|
|
1491
|
+
const truncated = treeRecord.truncated === true;
|
|
1298
1492
|
return {
|
|
1299
1493
|
ref: treeRef,
|
|
1300
|
-
tree: Array.isArray(treeRecord.tree) ? treeRecord.tree : []
|
|
1494
|
+
tree: Array.isArray(treeRecord.tree) ? treeRecord.tree : [],
|
|
1495
|
+
truncated
|
|
1301
1496
|
};
|
|
1302
1497
|
}
|
|
1303
1498
|
/** Collect every file inside a skill directory, including SKILL.md itself. */
|
|
@@ -1525,6 +1720,27 @@ async function getLatestReleaseTag(repo, fetchImpl = fetch) {
|
|
|
1525
1720
|
const tag = typeof payload === "object" && payload !== null ? payload.tag_name : void 0;
|
|
1526
1721
|
return typeof tag === "string" && tag !== "" ? tag : void 0;
|
|
1527
1722
|
}
|
|
1723
|
+
/** Release tags of a repo, newest first (skips drafts, keeps prereleases). */
|
|
1724
|
+
async function listRepoReleases(repo, fetchImpl = fetch) {
|
|
1725
|
+
let response;
|
|
1726
|
+
try {
|
|
1727
|
+
response = await fetchImpl(`https://api.github.com/repos/${repo}/releases?per_page=20`, { headers: {
|
|
1728
|
+
accept: "application/vnd.github+json",
|
|
1729
|
+
...githubAuthHeaders()
|
|
1730
|
+
} });
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
throw new RepoFetchError("github releases request failed: " + (error instanceof Error ? error.message : String(error)));
|
|
1733
|
+
}
|
|
1734
|
+
if (!response.ok) throw fetchError("github releases lookup failed", response);
|
|
1735
|
+
let payload;
|
|
1736
|
+
try {
|
|
1737
|
+
payload = await response.json();
|
|
1738
|
+
} catch {
|
|
1739
|
+
throw new RepoFetchError("invalid github releases response");
|
|
1740
|
+
}
|
|
1741
|
+
if (!Array.isArray(payload)) return [];
|
|
1742
|
+
return payload.filter((item) => typeof item === "object" && item !== null && item.draft !== true).map((item) => item.tag_name).filter((tag) => typeof tag === "string" && tag !== "");
|
|
1743
|
+
}
|
|
1528
1744
|
/** Branch names of a repo, default branch first (one GitHub API request). */
|
|
1529
1745
|
async function listRepoBranches(repo, fetchImpl = fetch) {
|
|
1530
1746
|
let response;
|
|
@@ -2045,15 +2261,23 @@ async function buildCatalog(deps, cwd) {
|
|
|
2045
2261
|
title: ""
|
|
2046
2262
|
}];
|
|
2047
2263
|
}
|
|
2048
|
-
const
|
|
2264
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2265
|
+
const identitiesByKey = /* @__PURE__ */ new Map();
|
|
2049
2266
|
let complete = true;
|
|
2050
2267
|
for (const ws of workspaces) {
|
|
2051
2268
|
const lookup = ws.path !== "" ? { cwd: ws.path } : void 0;
|
|
2052
2269
|
const snapshot = await deps.skills.snapshot(lookup);
|
|
2053
2270
|
if (!snapshot.complete) complete = false;
|
|
2054
2271
|
for (const skill of snapshot.skills) {
|
|
2055
|
-
|
|
2056
|
-
|
|
2272
|
+
const logicalKey = isProjectSource(skill.source) && ws.path !== "" ? `${skill.name}\0${ws.path}\0${skill.source}` : skill.name;
|
|
2273
|
+
let identities = identitiesByKey.get(logicalKey);
|
|
2274
|
+
if (identities === void 0) {
|
|
2275
|
+
identities = /* @__PURE__ */ new Set();
|
|
2276
|
+
identitiesByKey.set(logicalKey, identities);
|
|
2277
|
+
}
|
|
2278
|
+
identities.add(skill.source + "\0" + skill.provider);
|
|
2279
|
+
if (byKey.has(logicalKey)) continue;
|
|
2280
|
+
byKey.set(logicalKey, {
|
|
2057
2281
|
skill,
|
|
2058
2282
|
...isProjectSource(skill.source) && ws.path !== "" ? {
|
|
2059
2283
|
workspace: ws.path,
|
|
@@ -2062,6 +2286,17 @@ async function buildCatalog(deps, cwd) {
|
|
|
2062
2286
|
});
|
|
2063
2287
|
}
|
|
2064
2288
|
}
|
|
2289
|
+
const disabled = await deps.store.listDisabled();
|
|
2290
|
+
const byName = byKey;
|
|
2291
|
+
for (const d of disabled) {
|
|
2292
|
+
let identities = identitiesByKey.get(d.name);
|
|
2293
|
+
if (identities === void 0) {
|
|
2294
|
+
identities = /* @__PURE__ */ new Set();
|
|
2295
|
+
identitiesByKey.set(d.name, identities);
|
|
2296
|
+
}
|
|
2297
|
+
identities.add("hub-disabled\0" + d.root);
|
|
2298
|
+
}
|
|
2299
|
+
const duplicateNames = [...identitiesByKey.entries()].filter(([, identities]) => identities.size > 1).map(([key]) => key.split("\0")[0]).filter((name, idx, arr) => arr.indexOf(name) === idx).sort((a, b) => a.localeCompare(b));
|
|
2065
2300
|
const timesByName = /* @__PURE__ */ new Map();
|
|
2066
2301
|
await Promise.all([...byName.values()].map(async ({ skill }) => {
|
|
2067
2302
|
if (!isWritableSource(skill.source)) return;
|
|
@@ -2079,6 +2314,22 @@ async function buildCatalog(deps, cwd) {
|
|
|
2079
2314
|
return;
|
|
2080
2315
|
} catch {}
|
|
2081
2316
|
}));
|
|
2317
|
+
const interfaceByName = /* @__PURE__ */ new Map();
|
|
2318
|
+
await Promise.all([...byName.values()].map(async ({ skill, workspace }) => {
|
|
2319
|
+
const candidates = [];
|
|
2320
|
+
if (isWritableSource(skill.source)) candidates.push(join(rootPath(skill.source, home), skill.name));
|
|
2321
|
+
else if (isProjectSource(skill.source) && workspace !== void 0) candidates.push(join(workspace, skill.source === "project-dsh" ? ".dsh/skills" : ".agents/skills", skill.name));
|
|
2322
|
+
else if (isProjectSource(skill.source)) {
|
|
2323
|
+
for (const ws of workspaces) if (ws.path !== "") candidates.push(join(ws.path, skill.source === "project-dsh" ? ".dsh/skills" : ".agents/skills", skill.name));
|
|
2324
|
+
}
|
|
2325
|
+
for (const dir of candidates) try {
|
|
2326
|
+
const iface = await readSkillInterface(dir);
|
|
2327
|
+
if (iface !== void 0) {
|
|
2328
|
+
interfaceByName.set(skill.name, iface);
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
} catch {}
|
|
2332
|
+
}));
|
|
2082
2333
|
const skills = [...byName.values()].map(({ skill, workspace, workspaceTitle }) => {
|
|
2083
2334
|
const row = {
|
|
2084
2335
|
name: skill.name,
|
|
@@ -2101,9 +2352,17 @@ async function buildCatalog(deps, cwd) {
|
|
|
2101
2352
|
row.addedAt = times.addedAt;
|
|
2102
2353
|
row.updatedAt = times.updatedAt;
|
|
2103
2354
|
}
|
|
2355
|
+
const iface = interfaceByName.get(skill.name);
|
|
2356
|
+
if (iface !== void 0) {
|
|
2357
|
+
if (iface.displayName !== void 0) row.displayName = iface.displayName;
|
|
2358
|
+
if (iface.shortDescription !== void 0) row.shortDescription = iface.shortDescription;
|
|
2359
|
+
if (iface.brandColor !== void 0) row.brandColor = iface.brandColor;
|
|
2360
|
+
if (iface.iconSmall !== void 0) row.iconSmall = iface.iconSmall;
|
|
2361
|
+
if (iface.iconLarge !== void 0) row.iconLarge = iface.iconLarge;
|
|
2362
|
+
if (iface.defaultPrompt !== void 0) row.defaultPrompt = iface.defaultPrompt;
|
|
2363
|
+
}
|
|
2104
2364
|
return row;
|
|
2105
2365
|
});
|
|
2106
|
-
const disabled = await deps.store.listDisabled();
|
|
2107
2366
|
const diagnostics = [...await scanDiagnostics("user-dsh", home), ...await scanDiagnostics("user-agents", home)];
|
|
2108
2367
|
return {
|
|
2109
2368
|
ok: true,
|
|
@@ -2111,7 +2370,8 @@ async function buildCatalog(deps, cwd) {
|
|
|
2111
2370
|
complete,
|
|
2112
2371
|
skills,
|
|
2113
2372
|
disabled,
|
|
2114
|
-
diagnostics
|
|
2373
|
+
diagnostics,
|
|
2374
|
+
...duplicateNames.length > 0 ? { duplicateNames } : {}
|
|
2115
2375
|
};
|
|
2116
2376
|
}
|
|
2117
2377
|
/** Map a loaded definition onto the wire shape. */
|
|
@@ -2235,15 +2495,57 @@ function makeRoutes(deps) {
|
|
|
2235
2495
|
if (skill === void 0) skill = await deps.skills.get(name);
|
|
2236
2496
|
}
|
|
2237
2497
|
if (skill === void 0) {
|
|
2498
|
+
const record = await deps.store.getDisabled(name);
|
|
2499
|
+
if (record !== void 0) try {
|
|
2500
|
+
const parsed = parseFrontmatter(await readFile(record.path, "utf8"));
|
|
2501
|
+
if (!("error" in parsed)) {
|
|
2502
|
+
const disabledDetail = {
|
|
2503
|
+
name: record.name,
|
|
2504
|
+
description: parsed.value.description,
|
|
2505
|
+
...parsed.value.whenToUse !== void 0 ? { whenToUse: parsed.value.whenToUse } : {},
|
|
2506
|
+
invocation: { ...parsed.value.invocation },
|
|
2507
|
+
provider: "skill-hub (disabled)",
|
|
2508
|
+
path: record.path,
|
|
2509
|
+
content: parsed.value.content
|
|
2510
|
+
};
|
|
2511
|
+
try {
|
|
2512
|
+
const times = await stat(record.path);
|
|
2513
|
+
disabledDetail.addedAt = times.birthtimeMs;
|
|
2514
|
+
disabledDetail.updatedAt = times.mtimeMs;
|
|
2515
|
+
} catch {}
|
|
2516
|
+
writeJson(res, 200, {
|
|
2517
|
+
ok: true,
|
|
2518
|
+
skill: disabledDetail
|
|
2519
|
+
});
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
} catch {}
|
|
2238
2523
|
writeError(res, 404, "skill not found: " + name);
|
|
2239
2524
|
return;
|
|
2240
2525
|
}
|
|
2241
2526
|
const detail = toDetail(skill);
|
|
2242
|
-
if (skill.path !== void 0)
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2527
|
+
if (skill.path !== void 0) {
|
|
2528
|
+
try {
|
|
2529
|
+
const times = await stat(skill.path);
|
|
2530
|
+
detail.addedAt = times.birthtimeMs;
|
|
2531
|
+
detail.updatedAt = times.mtimeMs;
|
|
2532
|
+
} catch {}
|
|
2533
|
+
try {
|
|
2534
|
+
const rb = skill.resourceBase;
|
|
2535
|
+
const dir = rb?.kind === "directory" && typeof rb.path === "string" ? rb.path : skill.path.endsWith("SKILL.md") ? dirname(skill.path) : void 0;
|
|
2536
|
+
if (dir !== void 0) {
|
|
2537
|
+
const iface = await readSkillInterface(dir);
|
|
2538
|
+
if (iface !== void 0) {
|
|
2539
|
+
if (iface.displayName !== void 0) detail.displayName = iface.displayName;
|
|
2540
|
+
if (iface.shortDescription !== void 0) detail.shortDescription = iface.shortDescription;
|
|
2541
|
+
if (iface.brandColor !== void 0) detail.brandColor = iface.brandColor;
|
|
2542
|
+
if (iface.iconSmall !== void 0) detail.iconSmall = iface.iconSmall;
|
|
2543
|
+
if (iface.iconLarge !== void 0) detail.iconLarge = iface.iconLarge;
|
|
2544
|
+
if (iface.defaultPrompt !== void 0) detail.defaultPrompt = iface.defaultPrompt;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
} catch {}
|
|
2548
|
+
}
|
|
2247
2549
|
writeJson(res, 200, {
|
|
2248
2550
|
ok: true,
|
|
2249
2551
|
skill: detail
|
|
@@ -2610,6 +2912,31 @@ function makeRoutes(deps) {
|
|
|
2610
2912
|
});
|
|
2611
2913
|
}
|
|
2612
2914
|
}),
|
|
2915
|
+
route({
|
|
2916
|
+
path: SKILL_HUB_API.marketSourceVersions,
|
|
2917
|
+
methods: ["GET"],
|
|
2918
|
+
handler: async ({ res, url }) => {
|
|
2919
|
+
const parsed = normalizeRepoInput(queryParam(url, "repo") ?? "");
|
|
2920
|
+
if (parsed === null) {
|
|
2921
|
+
writeError(res, 400, "repo must be owner/repo or a github.com URL");
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const repo = repoSlug(parsed);
|
|
2925
|
+
const source = await deps.store.getMarketSource(repo);
|
|
2926
|
+
if (source === void 0) {
|
|
2927
|
+
writeError(res, 404, "market source not found: " + repo);
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const [releases, branches] = await Promise.all([listRepoReleases(repo), listRepoBranches(repo)]);
|
|
2931
|
+
writeJson(res, 200, {
|
|
2932
|
+
ok: true,
|
|
2933
|
+
repo,
|
|
2934
|
+
...source.ref !== void 0 && source.ref !== "" ? { current: source.ref } : {},
|
|
2935
|
+
releases,
|
|
2936
|
+
branches
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
}),
|
|
2613
2940
|
route({
|
|
2614
2941
|
path: SKILL_HUB_API.marketCheck,
|
|
2615
2942
|
methods: ["GET"],
|
|
@@ -2725,8 +3052,11 @@ function makeRoutes(deps) {
|
|
|
2725
3052
|
}
|
|
2726
3053
|
if (ref === void 0 || ref === "") {
|
|
2727
3054
|
const releaseTag = await getLatestReleaseTag(repo);
|
|
2728
|
-
if (releaseTag !== void 0)
|
|
2729
|
-
|
|
3055
|
+
if (releaseTag !== void 0) {
|
|
3056
|
+
ref = releaseTag;
|
|
3057
|
+
const existing = await deps.store.getMarketSource(repo);
|
|
3058
|
+
if (existing !== void 0 && (existing.ref === void 0 || existing.ref === "")) await deps.store.setMarketSourceRef(repo, releaseTag);
|
|
3059
|
+
} else {
|
|
2730
3060
|
const branches = await listRepoBranches(repo);
|
|
2731
3061
|
if (branches.length === 0) {
|
|
2732
3062
|
writeError(res, 404, "repo has no branches to scan");
|
|
@@ -2742,12 +3072,13 @@ function makeRoutes(deps) {
|
|
|
2742
3072
|
return;
|
|
2743
3073
|
}
|
|
2744
3074
|
}
|
|
2745
|
-
const { ref: resolvedRef, tree } = await loadRepoTree(repo, ref);
|
|
3075
|
+
const { ref: resolvedRef, tree, truncated } = await loadRepoTree(repo, ref);
|
|
2746
3076
|
writeJson(res, 200, {
|
|
2747
3077
|
ok: true,
|
|
2748
3078
|
repo,
|
|
2749
3079
|
ref: resolvedRef,
|
|
2750
|
-
entries: discoverRepoEntries(tree, repo, await knownSkillNames(deps))
|
|
3080
|
+
entries: discoverRepoEntries(tree, repo, await knownSkillNames(deps)),
|
|
3081
|
+
...truncated ? { truncated: true } : {}
|
|
2751
3082
|
});
|
|
2752
3083
|
}
|
|
2753
3084
|
}),
|
|
@@ -3412,6 +3743,29 @@ function makeRoutes(deps) {
|
|
|
3412
3743
|
failed
|
|
3413
3744
|
});
|
|
3414
3745
|
}
|
|
3746
|
+
}),
|
|
3747
|
+
route({
|
|
3748
|
+
path: SKILL_HUB_API.diagnosticFix,
|
|
3749
|
+
methods: ["POST"],
|
|
3750
|
+
jsonBody: true,
|
|
3751
|
+
handler: async ({ res, body }) => {
|
|
3752
|
+
const request = body;
|
|
3753
|
+
const rawPath = typeof request.path === "string" ? request.path.trim() : "";
|
|
3754
|
+
if (rawPath === "") {
|
|
3755
|
+
writeError(res, 400, "path is required");
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
try {
|
|
3759
|
+
await fixDiagnosticFile(rawPath, homeOf(deps));
|
|
3760
|
+
deps.invalidate?.();
|
|
3761
|
+
writeJson(res, 200, {
|
|
3762
|
+
ok: true,
|
|
3763
|
+
path: rawPath
|
|
3764
|
+
});
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
writeRouteError(res, error);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3415
3769
|
})
|
|
3416
3770
|
];
|
|
3417
3771
|
}
|
|
@@ -9,8 +9,6 @@ import { type ReactElement } from 'react';
|
|
|
9
9
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
10
10
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
11
11
|
import { type CardShell, type FieldState, type FormScope } from './settings-form.ts';
|
|
12
|
-
import { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR } from './panel/format.ts';
|
|
13
|
-
export { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR };
|
|
14
12
|
/** The card's projected state. */
|
|
15
13
|
export interface SkillHubSettingsState extends CardShell {
|
|
16
14
|
enabled: FieldState;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Browser-side API client for the /api/skill-hub route family. The only
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
|
-
import { type CatalogResponse, type CollectionGroup, type ConfigRequest, type ConfigResponse, type CreateRequest, type CreateResponse, type MarketCheckResponse, type MarketSourcesResponse, type MarketSyncResponse, type RepoDiscoverResponse, type RepoImportCancelResponse, type RepoImportProgressResponse, type RepoImportResponse, type SkillDetail, type SkillDeleteResponse, type SkillTag, type SourceCheckResponse, type SourceDeleteResponse, type SourceRestoreResponse, type SourceTrashClearResponse, type SourcesResponse, type SourceSyncResponse, type StatsResponse, type ToggleBatchResponse, type UpdateCheckResponse } from '../protocol.ts';
|
|
5
|
+
import { type CatalogResponse, type CollectionGroup, type ConfigRequest, type ConfigResponse, type CreateRequest, type CreateResponse, type MarketCheckResponse, type MarketSourcesResponse, type MarketSourceVersionsResponse, type MarketSyncResponse, type RepoDiscoverResponse, type RepoImportCancelResponse, type RepoImportProgressResponse, type RepoImportResponse, type SkillDetail, type SkillDeleteResponse, type SkillTag, type SourceCheckResponse, type SourceDeleteResponse, type SourceRestoreResponse, type SourceTrashClearResponse, type SourcesResponse, type SourceSyncResponse, type StatsResponse, type ToggleBatchResponse, type UpdateCheckResponse } from '../protocol.ts';
|
|
6
6
|
/** Error carrying the route's JSON error message. */
|
|
7
7
|
export declare class SkillHubApiError extends Error {
|
|
8
8
|
constructor(message: string);
|
|
@@ -35,6 +35,8 @@ export declare class SkillHubApi {
|
|
|
35
35
|
marketCheck(): Promise<MarketCheckResponse>;
|
|
36
36
|
/** Align a market source to its pinned ref; resolves with tracked skills. */
|
|
37
37
|
marketSync(repo: string): Promise<MarketSyncResponse>;
|
|
38
|
+
/** Releases + branches of a market source for the version picker. */
|
|
39
|
+
marketSourceVersions(repo: string): Promise<MarketSourceVersionsResponse>;
|
|
38
40
|
/** Pin a market source to an explicit ref (branch picker). */
|
|
39
41
|
setMarketSourceRef(repo: string, ref: string): Promise<MarketSourcesResponse>;
|
|
40
42
|
/** Check the hub's own latest GitHub release. */
|
|
@@ -77,4 +79,6 @@ export declare class SkillHubApi {
|
|
|
77
79
|
restoreSource(name: string): Promise<SourceRestoreResponse>;
|
|
78
80
|
/** Permanently delete every skill currently in the trash. */
|
|
79
81
|
clearTrash(): Promise<SourceTrashClearResponse>;
|
|
82
|
+
/** Auto-fix a fixable diagnostic (e.g. unquoted colon). */
|
|
83
|
+
fixDiagnostic(path: string): Promise<import('../protocol.ts').DiagnosticFixResponse>;
|
|
80
84
|
}
|
|
@@ -9,3 +9,13 @@ export declare function dictionary(): Record<string, string>;
|
|
|
9
9
|
export declare function tt(key: HubKey, values?: TranslateValues): string;
|
|
10
10
|
/** Human-readable error text from an unknown thrown value. */
|
|
11
11
|
export declare function errorMessage(error: unknown): string;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a displayName is worth showing beside the kebab-case name.
|
|
14
|
+
* Pure case/punctuation variants (e.g. "Code Review" vs "code-review") are redundant.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isDisplayNameDistinct(name: string, displayName: string | undefined): displayName is string;
|
|
17
|
+
/**
|
|
18
|
+
* Copy text to the clipboard. Uses the async API with a positioned-textarea
|
|
19
|
+
* fallback for insecure contexts. Resolves true only when the copy succeeded.
|
|
20
|
+
*/
|
|
21
|
+
export declare function copyTextToClipboard(text: string): Promise<boolean>;
|