dsh-skill-hub 0.3.19 → 0.3.20
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/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +10 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +132 -94
- package/lib/types/client/api.d.ts +3 -3
- package/lib/types/protocol/config.d.ts +17 -11
- package/lib/types/protocol/market.d.ts +11 -0
- package/lib/types/protocol.d.ts +1 -1
- package/lib/types/repo/github-client.d.ts +11 -4
- package/lib/types/repo.d.ts +1 -1
- package/lib/types/routes/deps.d.ts +6 -1
- package/lib/types/skillfs/scan.d.ts +6 -5
- package/lib/types/skillfs.d.ts +25 -1
- package/package.json +21 -20
- package/src/client/api.ts +4 -3
- package/src/client/panel/hooks/useMarketFlow.ts +4 -2
- package/src/client/settings-card.tsx +2 -1
- package/src/client/settings-form.ts +6 -3
- package/src/index.ts +1 -1
- package/src/protocol/config.ts +27 -24
- package/src/protocol/market.ts +12 -0
- package/src/protocol.ts +1 -0
- package/src/repo/github-client.ts +26 -14
- package/src/repo.test.ts +20 -1
- package/src/repo.ts +1 -1
- package/src/routes/catalog-data.ts +7 -4
- package/src/routes/catalog.ts +4 -9
- package/src/routes/deps.ts +7 -2
- package/src/routes/market.ts +13 -2
- package/src/routes/repo-import.ts +4 -7
- package/src/routes.test.ts +68 -0
- package/src/skillfs/scan.ts +30 -17
- package/src/skillfs.test.ts +64 -3
- package/src/skillfs.ts +39 -8
- package/src/update.ts +6 -6
package/lib/index.js
CHANGED
|
@@ -83,29 +83,30 @@ const HUB_CONFIG_DEFAULTS = {
|
|
|
83
83
|
statsScanMinutes: 5
|
|
84
84
|
};
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
86
|
+
* Fill a raw settings value into the effective hub config: schema defaults for
|
|
87
|
+
* anything missing, and the numeric stats knobs clamped to sane ranges
|
|
88
|
+
* (window ≥ 0, scan interval ≥ 1 minute).
|
|
89
|
+
*
|
|
90
|
+
* There is deliberately no "saved sidecar layer" parameter. Until dsh 0.1.7 the
|
|
91
|
+
* hub kept its own runtime overrides in the sidecar and merged them here; the
|
|
92
|
+
* settings namespace is now the only user layer (`updateConfig` writes through
|
|
93
|
+
* `ctx.settings.mutate`), so a merge against a sidecar layer could never
|
|
94
|
+
* observe anything but `{}` — see the git history of `resolveHubConfig`.
|
|
91
95
|
*/
|
|
92
|
-
function resolveHubConfig(
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const githubToken = saved.githubToken !== void 0 ? saved.githubToken : base.githubToken;
|
|
96
|
-
const windowDays = clampNumber(saved.statsWindowDays ?? base.statsWindowDays, 0) ?? HUB_CONFIG_DEFAULTS.statsWindowDays;
|
|
97
|
-
const scanMinutes = clampNumber(saved.statsScanMinutes ?? base.statsScanMinutes, 1) ?? HUB_CONFIG_DEFAULTS.statsScanMinutes;
|
|
96
|
+
function resolveHubConfig(base = {}) {
|
|
97
|
+
const windowDays = clampNumber(base.statsWindowDays, 0) ?? HUB_CONFIG_DEFAULTS.statsWindowDays;
|
|
98
|
+
const scanMinutes = clampNumber(base.statsScanMinutes, 1) ?? HUB_CONFIG_DEFAULTS.statsScanMinutes;
|
|
98
99
|
return {
|
|
99
|
-
enabled:
|
|
100
|
-
announceToAgent:
|
|
101
|
-
showUseCount:
|
|
102
|
-
showUseTime:
|
|
103
|
-
showGroupSummary:
|
|
100
|
+
enabled: base.enabled ?? HUB_CONFIG_DEFAULTS.enabled,
|
|
101
|
+
announceToAgent: base.announceToAgent ?? HUB_CONFIG_DEFAULTS.announceToAgent,
|
|
102
|
+
showUseCount: base.showUseCount ?? HUB_CONFIG_DEFAULTS.showUseCount,
|
|
103
|
+
showUseTime: base.showUseTime ?? HUB_CONFIG_DEFAULTS.showUseTime,
|
|
104
|
+
showGroupSummary: base.showGroupSummary ?? HUB_CONFIG_DEFAULTS.showGroupSummary,
|
|
104
105
|
statsWindowDays: windowDays,
|
|
105
106
|
statsScanMinutes: scanMinutes,
|
|
106
|
-
...dotModelColor !== void 0 ? { dotModelColor } : {},
|
|
107
|
-
...dotUserColor !== void 0 ? { dotUserColor } : {},
|
|
108
|
-
...githubToken !== void 0 ? { githubToken } : {}
|
|
107
|
+
...base.dotModelColor !== void 0 ? { dotModelColor: base.dotModelColor } : {},
|
|
108
|
+
...base.dotUserColor !== void 0 ? { dotUserColor: base.dotUserColor } : {},
|
|
109
|
+
...base.githubToken !== void 0 ? { githubToken: base.githubToken } : {}
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
/** Clamp a numeric override into a valid value; undefined/invalid stays undefined. */
|
|
@@ -1373,6 +1374,26 @@ function rootOfPath(path, home = dshHome()) {
|
|
|
1373
1374
|
}
|
|
1374
1375
|
}
|
|
1375
1376
|
//#endregion
|
|
1377
|
+
//#region src/concurrency.ts
|
|
1378
|
+
/**
|
|
1379
|
+
* 并发工具:有界并发的 map。原先 repo.ts 与 stats.ts 各有一份实现,
|
|
1380
|
+
* 这里统一为一份(采用 stats 版本的 limit 夹取,调用点 limit≥1 行为不变)。
|
|
1381
|
+
*/
|
|
1382
|
+
/** Run an async worker over items with a bounded concurrency, preserving index order. */
|
|
1383
|
+
async function mapConcurrent(items, limit, worker) {
|
|
1384
|
+
const results = new Array(items.length);
|
|
1385
|
+
let cursor = 0;
|
|
1386
|
+
const runners = Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
1387
|
+
while (cursor < items.length) {
|
|
1388
|
+
const index = cursor;
|
|
1389
|
+
cursor += 1;
|
|
1390
|
+
results[index] = await worker(items[index], index);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
await Promise.all(runners);
|
|
1394
|
+
return results;
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1376
1397
|
//#region src/skillfs/scan.ts
|
|
1377
1398
|
/**
|
|
1378
1399
|
* Scan one skills root for discovery files: directory bundles (SKILL.md)
|
|
@@ -1475,7 +1496,7 @@ async function readSkillInterface(directory) {
|
|
|
1475
1496
|
const hexColor = (value) => {
|
|
1476
1497
|
if (typeof value !== "string") return void 0;
|
|
1477
1498
|
const trimmed = value.trim();
|
|
1478
|
-
return
|
|
1499
|
+
return HEX_COLOR_RE.test(trimmed) ? trimmed : void 0;
|
|
1479
1500
|
};
|
|
1480
1501
|
const iconPath = (value) => {
|
|
1481
1502
|
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
@@ -1515,37 +1536,38 @@ async function findProjectRoot(cwd) {
|
|
|
1515
1536
|
return resolve(cwd);
|
|
1516
1537
|
}
|
|
1517
1538
|
/**
|
|
1518
|
-
* Scan one writable root for files the provider ignores, so the GUI can
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1539
|
+
* Scan one writable root for files the provider ignores, so the GUI can show
|
|
1540
|
+
* why a skill never appears — a missing frontmatter, a name that disagrees
|
|
1541
|
+
* with the discovery path, and the like must be visible, not silent. Files the
|
|
1542
|
+
* hub itself disabled (`.disabled`) are skipped: they are a toggle state, not
|
|
1543
|
+
* a defect.
|
|
1522
1544
|
*/
|
|
1523
1545
|
async function scanDiagnostics(root, home = dshHome()) {
|
|
1524
|
-
|
|
1525
|
-
|
|
1546
|
+
return (await mapConcurrent(await listSkillEntries(root, home), 8, async (entry) => {
|
|
1547
|
+
const found = [];
|
|
1526
1548
|
let text;
|
|
1527
1549
|
try {
|
|
1528
1550
|
text = await readFile(entry.path, "utf8");
|
|
1529
1551
|
} catch (error) {
|
|
1530
|
-
if (error.code !== "ENOENT")
|
|
1552
|
+
if (error.code !== "ENOENT") found.push({
|
|
1531
1553
|
path: entry.path,
|
|
1532
1554
|
root,
|
|
1533
1555
|
reason: "unreadable: " + errorText(error)
|
|
1534
1556
|
});
|
|
1535
|
-
|
|
1557
|
+
return found;
|
|
1536
1558
|
}
|
|
1537
1559
|
const parsed = parseFrontmatter(text);
|
|
1538
1560
|
if ("error" in parsed) {
|
|
1539
1561
|
const fixable = repairFrontmatterFileText(text) !== null;
|
|
1540
|
-
|
|
1562
|
+
found.push({
|
|
1541
1563
|
path: entry.path,
|
|
1542
1564
|
root,
|
|
1543
1565
|
reason: parsed.error,
|
|
1544
1566
|
...fixable ? { fixable: true } : {}
|
|
1545
1567
|
});
|
|
1546
|
-
|
|
1568
|
+
return found;
|
|
1547
1569
|
}
|
|
1548
|
-
if (repairFrontmatterFileText(text) !== null)
|
|
1570
|
+
if (repairFrontmatterFileText(text) !== null) found.push({
|
|
1549
1571
|
path: entry.path,
|
|
1550
1572
|
root,
|
|
1551
1573
|
reason: "frontmatter contains unquoted colon/bracket (auto-repaired in memory; click Fix to persist)",
|
|
@@ -1553,18 +1575,18 @@ async function scanDiagnostics(root, home = dshHome()) {
|
|
|
1553
1575
|
});
|
|
1554
1576
|
const { value } = parsed;
|
|
1555
1577
|
const pathName = entry.kind === "directory" ? basename(entry.directory) : basename(entry.path, ".md");
|
|
1556
|
-
if (value.name !== pathName)
|
|
1578
|
+
if (value.name !== pathName) found.push({
|
|
1557
1579
|
path: entry.path,
|
|
1558
1580
|
root,
|
|
1559
1581
|
reason: `frontmatter name "${value.name}" does not match the discovery path "${pathName}" (the provider registers by path)`
|
|
1560
1582
|
});
|
|
1561
|
-
if (value.description.length < 10)
|
|
1583
|
+
if (value.description.length < 10) found.push({
|
|
1562
1584
|
path: entry.path,
|
|
1563
1585
|
root,
|
|
1564
1586
|
reason: `description is only ${value.description.length} chars; write a one-line description so agents can auto-activate this skill`
|
|
1565
1587
|
});
|
|
1566
|
-
|
|
1567
|
-
|
|
1588
|
+
return found;
|
|
1589
|
+
})).flat();
|
|
1568
1590
|
}
|
|
1569
1591
|
//#endregion
|
|
1570
1592
|
//#region src/skillfs.ts
|
|
@@ -1624,14 +1646,18 @@ async function enableSkill(disabledPath) {
|
|
|
1624
1646
|
return original;
|
|
1625
1647
|
}
|
|
1626
1648
|
/**
|
|
1627
|
-
* Move
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1649
|
+
* Move one already-resolved skill (a bundle directory or a flat .md file) into
|
|
1650
|
+
* its root's trash as `<root>/.trash/<name>-<timestamp>`.
|
|
1651
|
+
*
|
|
1652
|
+
* The layout is the restore contract, not an implementation detail:
|
|
1653
|
+
* `restoreSkill` renames the entry back and only proves the parent is `.trash`,
|
|
1654
|
+
* and `clearTrash` deletes in place. Every producer of a trash entry must go
|
|
1655
|
+
* through here — skill delete, the hub-disabled whole-group delete, and repo
|
|
1656
|
+
* import's placeholder replacement all move skills for the same recovery flow.
|
|
1657
|
+
*
|
|
1631
1658
|
* @returns the trashed path plus the original path that was moved.
|
|
1632
1659
|
*/
|
|
1633
|
-
async function
|
|
1634
|
-
const source = basename(sourcePath) === "SKILL.md" ? dirname(sourcePath) : sourcePath;
|
|
1660
|
+
async function moveIntoTrash(source) {
|
|
1635
1661
|
const trashDir = join(dirname(source), ".trash");
|
|
1636
1662
|
await mkdir(trashDir, { recursive: true });
|
|
1637
1663
|
const target = join(trashDir, basename(source) + "-" + Date.now());
|
|
@@ -1641,6 +1667,16 @@ async function trashSkill(sourcePath) {
|
|
|
1641
1667
|
source
|
|
1642
1668
|
};
|
|
1643
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Move a skill (directory bundle or flat .md file) into the root's trash.
|
|
1672
|
+
* The skill stays recoverable: the source is renamed into
|
|
1673
|
+
* <root>/.trash/<name>-<timestamp>. A directory bundle is addressed by its
|
|
1674
|
+
* SKILL.md path and moved as a whole directory.
|
|
1675
|
+
* @returns the trashed path plus the original path that was moved.
|
|
1676
|
+
*/
|
|
1677
|
+
async function trashSkill(sourcePath) {
|
|
1678
|
+
return moveIntoTrash(basename(sourcePath) === "SKILL.md" ? dirname(sourcePath) : sourcePath);
|
|
1679
|
+
}
|
|
1644
1680
|
/** Restore a trashed skill (directory or flat file) to its original location. */
|
|
1645
1681
|
async function restoreSkill(entry, home = dshHome()) {
|
|
1646
1682
|
if (basename(dirname(entry.path)) !== ".trash") throw new TypeError("not a trashed skill path: " + entry.path);
|
|
@@ -1663,14 +1699,29 @@ function skillFileKind(path) {
|
|
|
1663
1699
|
if (basename(path) === "SKILL.md") return "directory";
|
|
1664
1700
|
if (path.endsWith(".md")) return "flat";
|
|
1665
1701
|
}
|
|
1666
|
-
/**
|
|
1702
|
+
/**
|
|
1703
|
+
* Repair one file on disk when its frontmatter is auto-fixable. Returns the new text.
|
|
1704
|
+
*
|
|
1705
|
+
* The write is atomic (sibling temp file + rename), matching the sidecar's
|
|
1706
|
+
* persist path: this rewrites a real skill file in place, and a crash midway
|
|
1707
|
+
* through a plain write would leave a truncated SKILL.md — exactly the failure
|
|
1708
|
+
* the store layer already guards against. The `.tmp` sibling is never picked
|
|
1709
|
+
* up by discovery (only `SKILL.md` and `*.md` are scanned).
|
|
1710
|
+
*/
|
|
1667
1711
|
async function fixDiagnosticFile(path, home = dshHome()) {
|
|
1668
1712
|
if (rootOfPath(path, home) === void 0) throw new TypeError("not a hub writable skill path: " + path);
|
|
1669
1713
|
const repairedText = repairFrontmatterFileText(await readFile(path, "utf8"));
|
|
1670
1714
|
if (repairedText === null) throw new TypeError("diagnostic is not auto-fixable: " + path);
|
|
1671
1715
|
const parsed = parseFrontmatter(repairedText);
|
|
1672
1716
|
if ("error" in parsed) throw new TypeError("repaired frontmatter still invalid: " + parsed.error);
|
|
1673
|
-
|
|
1717
|
+
const tmp = path + ".tmp";
|
|
1718
|
+
await writeFile(tmp, repairedText, "utf8");
|
|
1719
|
+
try {
|
|
1720
|
+
await rename(tmp, path);
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1674
1725
|
return path;
|
|
1675
1726
|
}
|
|
1676
1727
|
//#endregion
|
|
@@ -1913,15 +1964,27 @@ function etagCacheSet(url, etag, json) {
|
|
|
1913
1964
|
});
|
|
1914
1965
|
}
|
|
1915
1966
|
/**
|
|
1967
|
+
* When the response shows an exhausted GitHub rate limit, the instant it
|
|
1968
|
+
* resets; otherwise undefined. Shared by the repo fetch layer and the plugin's
|
|
1969
|
+
* own update check so both agree on what counts as a rate-limit rejection
|
|
1970
|
+
* (403/429 + `x-ratelimit-remaining: 0` + a parseable `x-ratelimit-reset`)
|
|
1971
|
+
* instead of each re-deriving it from the headers.
|
|
1972
|
+
*/
|
|
1973
|
+
function rateLimitResetAt(response) {
|
|
1974
|
+
if (response.status !== 403 && response.status !== 429) return void 0;
|
|
1975
|
+
if (response.headers.get("x-ratelimit-remaining") !== "0") return void 0;
|
|
1976
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
1977
|
+
if (reset === null) return void 0;
|
|
1978
|
+
const at = /* @__PURE__ */ new Date(Number(reset) * 1e3);
|
|
1979
|
+
return Number.isNaN(at.getTime()) ? void 0 : at;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1916
1982
|
* Build a RepoFetchError; when the response shows an exhausted rate limit,
|
|
1917
1983
|
* report the reset time instead of a bare HTTP status.
|
|
1918
1984
|
*/
|
|
1919
1985
|
function fetchError(context, response, fallbackStatus = 502) {
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
if (response.status === 403 || response.status === 429) {
|
|
1923
|
-
if (remaining === "0" && reset !== null) return new RepoFetchError("github rate limit reached (anonymous quota exhausted); retry after " + (/* @__PURE__ */ new Date(Number(reset) * 1e3)).toLocaleTimeString() + " or set GITHUB_TOKEN", 403);
|
|
1924
|
-
}
|
|
1986
|
+
const resetAt = rateLimitResetAt(response);
|
|
1987
|
+
if (resetAt !== void 0) return new RepoFetchError("github rate limit reached (anonymous quota exhausted); retry after " + resetAt.toLocaleTimeString() + " or set GITHUB_TOKEN", 403);
|
|
1925
1988
|
return new RepoFetchError(context + " (HTTP " + response.status + ")", response.status === 404 ? 404 : fallbackStatus);
|
|
1926
1989
|
}
|
|
1927
1990
|
/** 是否限流/中断类错误(调用方可直接透出,不再包装)。 */
|
|
@@ -1951,10 +2014,9 @@ async function fetchJson(url, fetchImpl, context, headers = apiHeaders()) {
|
|
|
1951
2014
|
}
|
|
1952
2015
|
}
|
|
1953
2016
|
/**
|
|
1954
|
-
* Load
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
* the exact same content a later download would fetch.
|
|
2017
|
+
* Load one JSON document from the GitHub API, reusing an ETag when the URL was
|
|
2018
|
+
* fetched before. `fetchImpl`/`context` exist so callers can stub the transport
|
|
2019
|
+
* and shape the error text; the payload is whatever the endpoint returned.
|
|
1958
2020
|
*/
|
|
1959
2021
|
async function fetchJsonCached(url, fetchImpl, context) {
|
|
1960
2022
|
const cached = etagCache.get(url);
|
|
@@ -2081,26 +2143,6 @@ async function loadRepoTreeAt(repo, treeSha, fetchImpl = fetch) {
|
|
|
2081
2143
|
return Array.isArray(record.tree) ? record.tree : [];
|
|
2082
2144
|
}
|
|
2083
2145
|
//#endregion
|
|
2084
|
-
//#region src/concurrency.ts
|
|
2085
|
-
/**
|
|
2086
|
-
* 并发工具:有界并发的 map。原先 repo.ts 与 stats.ts 各有一份实现,
|
|
2087
|
-
* 这里统一为一份(采用 stats 版本的 limit 夹取,调用点 limit≥1 行为不变)。
|
|
2088
|
-
*/
|
|
2089
|
-
/** Run an async worker over items with a bounded concurrency, preserving index order. */
|
|
2090
|
-
async function mapConcurrent(items, limit, worker) {
|
|
2091
|
-
const results = new Array(items.length);
|
|
2092
|
-
let cursor = 0;
|
|
2093
|
-
const runners = Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
2094
|
-
while (cursor < items.length) {
|
|
2095
|
-
const index = cursor;
|
|
2096
|
-
cursor += 1;
|
|
2097
|
-
results[index] = await worker(items[index], index);
|
|
2098
|
-
}
|
|
2099
|
-
});
|
|
2100
|
-
await Promise.all(runners);
|
|
2101
|
-
return results;
|
|
2102
|
-
}
|
|
2103
|
-
//#endregion
|
|
2104
2146
|
//#region src/repo/install.ts
|
|
2105
2147
|
/**
|
|
2106
2148
|
* 技能目录下载与落地:单文件下载(raw 优先、contents 兜底)、整目录
|
|
@@ -2352,9 +2394,14 @@ function isWritableSource(source) {
|
|
|
2352
2394
|
}
|
|
2353
2395
|
/** The resolved hub config a route sees (the shared resolver fills defaults). */
|
|
2354
2396
|
function configOf(deps) {
|
|
2355
|
-
return resolveHubConfig(
|
|
2397
|
+
return resolveHubConfig(deps.config?.() ?? {});
|
|
2356
2398
|
}
|
|
2357
|
-
/**
|
|
2399
|
+
/**
|
|
2400
|
+
* The raw saved config layer as persisted in the sidecar, reported for display
|
|
2401
|
+
* only. Since dsh 0.1.7 the settings namespace is the one user layer, so this
|
|
2402
|
+
* is legacy data: nothing writes it any more and the effective config above
|
|
2403
|
+
* never merges it.
|
|
2404
|
+
*/
|
|
2358
2405
|
function savedOf(deps) {
|
|
2359
2406
|
return deps.saved?.() ?? {};
|
|
2360
2407
|
}
|
|
@@ -2502,9 +2549,8 @@ async function checkLatestRelease(repo = UPDATE_REPO, fetchImpl = fetch) {
|
|
|
2502
2549
|
}
|
|
2503
2550
|
if (response.status === 404) return failure("no release published yet");
|
|
2504
2551
|
if (!response.ok) {
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
if ((response.status === 403 || response.status === 429) && remaining === "0" && reset !== null) return failure("github rate limit reached; retry after " + (/* @__PURE__ */ new Date(Number(reset) * 1e3)).toLocaleTimeString() + " or set GITHUB_TOKEN");
|
|
2552
|
+
const resetAt = rateLimitResetAt(response);
|
|
2553
|
+
if (resetAt !== void 0) return failure("github rate limit reached; retry after " + resetAt.toLocaleTimeString() + " or set GITHUB_TOKEN");
|
|
2508
2554
|
return failure("GitHub HTTP " + response.status);
|
|
2509
2555
|
}
|
|
2510
2556
|
let payload;
|
|
@@ -2692,7 +2738,8 @@ async function buildCatalog(deps, cwd) {
|
|
|
2692
2738
|
if (iface !== void 0) applyInterface(row, iface);
|
|
2693
2739
|
return row;
|
|
2694
2740
|
});
|
|
2695
|
-
const
|
|
2741
|
+
const [userDshDiagnostics, userAgentsDiagnostics] = await Promise.all([scanDiagnostics("user-dsh", home), scanDiagnostics("user-agents", home)]);
|
|
2742
|
+
const diagnostics = [...userDshDiagnostics, ...userAgentsDiagnostics];
|
|
2696
2743
|
return {
|
|
2697
2744
|
ok: true,
|
|
2698
2745
|
pluginVersion: CURRENT_VERSION,
|
|
@@ -2881,15 +2928,7 @@ function catalogRoutes(deps) {
|
|
|
2881
2928
|
writeError(res, 409, "disabled skill path is outside the hub writable roots");
|
|
2882
2929
|
return;
|
|
2883
2930
|
}
|
|
2884
|
-
|
|
2885
|
-
const trashDir = join(dirname(sourceToTrash), ".trash");
|
|
2886
|
-
await mkdir(trashDir, { recursive: true });
|
|
2887
|
-
const target = join(trashDir, basename(sourceToTrash) + "-" + Date.now());
|
|
2888
|
-
await rename(sourceToTrash, target);
|
|
2889
|
-
trashResult = {
|
|
2890
|
-
path: target,
|
|
2891
|
-
source: sourceToTrash
|
|
2892
|
-
};
|
|
2931
|
+
trashResult = await moveIntoTrash(disabled.path.endsWith("SKILL.md.disabled") ? dirname(disabled.path) : disabled.path);
|
|
2893
2932
|
} else {
|
|
2894
2933
|
writeError(res, resolved.status, resolved.error);
|
|
2895
2934
|
return;
|
|
@@ -3303,6 +3342,7 @@ function marketRoutes(deps) {
|
|
|
3303
3342
|
}
|
|
3304
3343
|
try {
|
|
3305
3344
|
const latestTag = await getLatestReleaseTag(source.repo);
|
|
3345
|
+
lastMarketCheck.set(source.repo, now);
|
|
3306
3346
|
if (source.ref === void 0) {
|
|
3307
3347
|
results.push({
|
|
3308
3348
|
...base,
|
|
@@ -3313,7 +3353,6 @@ function marketRoutes(deps) {
|
|
|
3313
3353
|
continue;
|
|
3314
3354
|
}
|
|
3315
3355
|
const latest = await getLatestCommit(source.repo, source.ref);
|
|
3316
|
-
lastMarketCheck.set(source.repo, now);
|
|
3317
3356
|
const commitMoved = source.commitSha !== void 0 && latest.commitSha !== source.commitSha;
|
|
3318
3357
|
const newRelease = latestTag !== void 0 && latestTag !== source.ref;
|
|
3319
3358
|
results.push({
|
|
@@ -3358,9 +3397,11 @@ function marketRoutes(deps) {
|
|
|
3358
3397
|
return;
|
|
3359
3398
|
}
|
|
3360
3399
|
const ref = parsed.ref;
|
|
3400
|
+
const slug = repoSlug(parsed);
|
|
3361
3401
|
writeJson(res, 200, {
|
|
3362
3402
|
ok: true,
|
|
3363
|
-
|
|
3403
|
+
repo: slug,
|
|
3404
|
+
repos: await deps.store.addMarketSource(slug, ref)
|
|
3364
3405
|
});
|
|
3365
3406
|
}
|
|
3366
3407
|
},
|
|
@@ -3554,10 +3595,7 @@ async function clearDisabledPlaceholder(deps, targetRoot, name) {
|
|
|
3554
3595
|
const sourceToTrash = record.path.endsWith("SKILL.md.disabled") ? dirname(record.path) : record.path;
|
|
3555
3596
|
if (sourceToTrash !== dir) return false;
|
|
3556
3597
|
if (await pathExists(join(dir, "SKILL.md"))) return false;
|
|
3557
|
-
const
|
|
3558
|
-
await mkdir(trashDir, { recursive: true });
|
|
3559
|
-
const target = join(trashDir, name + "-" + Date.now());
|
|
3560
|
-
await rename(sourceToTrash, target);
|
|
3598
|
+
const { path: target } = await moveIntoTrash(sourceToTrash);
|
|
3561
3599
|
await deps.store.addTrash({
|
|
3562
3600
|
name,
|
|
3563
3601
|
path: target,
|
|
@@ -4776,7 +4814,7 @@ function apply(ctx, config) {
|
|
|
4776
4814
|
}
|
|
4777
4815
|
return out;
|
|
4778
4816
|
};
|
|
4779
|
-
const current = () => resolveHubConfig(
|
|
4817
|
+
const current = () => resolveHubConfig(readConfig());
|
|
4780
4818
|
/** The Settings service, when this deployment mounts it. Absent ⇒ no user layer to read or write. */
|
|
4781
4819
|
const settingsOf = () => ctx.get("settings");
|
|
4782
4820
|
const store = new SkillHubStore();
|
|
@@ -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 ConfigResponse, type CreateRequest, type CreateResponse, type DiagnosticFixResponse, type GroupsResponse, type MarketCheckResponse, type MarketSourcesResponse, type MarketSourceVersionsResponse, type MarketStatsResponse, 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 TagSaveRequest, type ToggleBatchResponse, type UpdateCheckResponse } from '../protocol.ts';
|
|
5
|
+
import { type CatalogResponse, type ConfigResponse, type CreateRequest, type CreateResponse, type DiagnosticFixResponse, type GroupsResponse, type MarketCheckResponse, type MarketSourceAddResponse, type MarketSourcesResponse, type MarketSourceVersionsResponse, type MarketStatsResponse, 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 TagSaveRequest, 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);
|
|
@@ -31,8 +31,8 @@ export declare class SkillHubApi {
|
|
|
31
31
|
toggleBatch(names: string[], enabled: boolean): Promise<ToggleBatchResponse>;
|
|
32
32
|
/** The user's added market sources. */
|
|
33
33
|
market(): Promise<MarketSourcesResponse>;
|
|
34
|
-
/** Add a market source repo; resolves with the fresh list. */
|
|
35
|
-
addMarketSource(repo: string): Promise<
|
|
34
|
+
/** Add a market source repo; resolves with the fresh list plus the resolved slug. */
|
|
35
|
+
addMarketSource(repo: string): Promise<MarketSourceAddResponse>;
|
|
36
36
|
/** Remove a market source repo; resolves with the fresh list. */
|
|
37
37
|
removeMarketSource(repo: string): Promise<MarketSourcesResponse>;
|
|
38
38
|
/** Stars + downloads per market source (hourly server-side cache; refresh=1 refetches stale entries). */
|
|
@@ -83,13 +83,17 @@ export declare const HUB_CONFIG_DEFAULTS: {
|
|
|
83
83
|
readonly statsScanMinutes: 5;
|
|
84
84
|
};
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
86
|
+
* Fill a raw settings value into the effective hub config: schema defaults for
|
|
87
|
+
* anything missing, and the numeric stats knobs clamped to sane ranges
|
|
88
|
+
* (window ≥ 0, scan interval ≥ 1 minute).
|
|
89
|
+
*
|
|
90
|
+
* There is deliberately no "saved sidecar layer" parameter. Until dsh 0.1.7 the
|
|
91
|
+
* hub kept its own runtime overrides in the sidecar and merged them here; the
|
|
92
|
+
* settings namespace is now the only user layer (`updateConfig` writes through
|
|
93
|
+
* `ctx.settings.mutate`), so a merge against a sidecar layer could never
|
|
94
|
+
* observe anything but `{}` — see the git history of `resolveHubConfig`.
|
|
91
95
|
*/
|
|
92
|
-
export declare function resolveHubConfig(
|
|
96
|
+
export declare function resolveHubConfig(base?: Partial<HubConfig>): HubConfig;
|
|
93
97
|
/**
|
|
94
98
|
* Drop the GitHub token from a config-shaped object. Returns a copy, so the
|
|
95
99
|
* caller's own config layer keeps the token. Shared by the config route's GET
|
|
@@ -112,7 +116,13 @@ export interface ConfigResponse {
|
|
|
112
116
|
/** True when a GitHub token is in effect (saved override or the base/env layer). */
|
|
113
117
|
githubTokenSet: boolean;
|
|
114
118
|
}
|
|
115
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* POST /api/skill-hub/config — a partial patch; omitted fields keep their values.
|
|
121
|
+
*
|
|
122
|
+
* Only the fields the handler actually parses belong here: the stats knobs are
|
|
123
|
+
* owned by the settings namespace (`statsWindowDays` / `statsScanMinutes` are
|
|
124
|
+
* still part of {@link HubSettingsValue}), not by this route.
|
|
125
|
+
*/
|
|
116
126
|
export interface ConfigRequest {
|
|
117
127
|
/** Set the field; null clears the saved override so it re-inherits the default. */
|
|
118
128
|
enabled?: boolean | null;
|
|
@@ -128,10 +138,6 @@ export interface ConfigRequest {
|
|
|
128
138
|
dotModelColor?: string | null;
|
|
129
139
|
/** Set the dot color; null clears the saved override so it re-inherits the default. */
|
|
130
140
|
dotUserColor?: string | null;
|
|
131
|
-
/** 统计滚动窗口天数(0 = 全部历史);null 清除覆盖回默认。 */
|
|
132
|
-
statsWindowDays?: number | null;
|
|
133
|
-
/** 自动统计扫描间隔(分钟);null 清除覆盖回默认。 */
|
|
134
|
-
statsScanMinutes?: number | null;
|
|
135
141
|
/** Set the GitHub token; null/empty clears it back to anonymous. */
|
|
136
142
|
githubToken?: string | null;
|
|
137
143
|
}
|
|
@@ -23,6 +23,17 @@ export interface MarketSourceResponse {
|
|
|
23
23
|
ok: true;
|
|
24
24
|
repos: MarketSourceRecord[];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/skill-hub/market/source — same list, plus the slug the host
|
|
28
|
+
* resolved from the input. Clients must scan *this* slug rather than assume
|
|
29
|
+
* the acted-on source sits at some position in `repos`: re-adding a repo the
|
|
30
|
+
* user already tracks only updates its `ref` in place and never reorders the
|
|
31
|
+
* list.
|
|
32
|
+
*/
|
|
33
|
+
export interface MarketSourceAddResponse extends MarketSourceResponse {
|
|
34
|
+
/** Normalized `owner/repo` the host recorded for the submitted input. */
|
|
35
|
+
repo: string;
|
|
36
|
+
}
|
|
26
37
|
/** POST /api/skill-hub/market/source/ref — pin a market source to an explicit ref. */
|
|
27
38
|
export interface MarketSourceRefRequest {
|
|
28
39
|
repo: string;
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { WritableRoot, HubInvocation, CatalogSkill, DisabledSkill, Diagnost
|
|
|
12
12
|
export type { SkillStat, StatsResponse, SkillStatsCheckpoint } from './protocol/stats.ts';
|
|
13
13
|
export type { ErrorResponse, HubConfig, HubSettingsValue, RedactedHubConfig, ConfigResponse, ConfigRequest } from './protocol/config.ts';
|
|
14
14
|
export { HUB_CONFIG_DEFAULTS, HUB_ENTRY_ID, HEX_COLOR_RE, GITHUB_TOKEN_RE, resolveHubConfig, redactGithubToken } from './protocol/config.ts';
|
|
15
|
-
export type { MarketSourceRecord, MarketSourcesResponse, MarketSourceRequest, MarketSourceResponse, MarketSourceRefRequest, MarketSourceVersionsResponse, MarketStatsSnapshot, MarketStatsResponse, MarketCheckResponse, MarketSyncResponse, } from './protocol/market.ts';
|
|
15
|
+
export type { MarketSourceRecord, MarketSourcesResponse, MarketSourceRequest, MarketSourceAddResponse, MarketSourceResponse, MarketSourceRefRequest, MarketSourceVersionsResponse, MarketStatsSnapshot, MarketStatsResponse, MarketCheckResponse, MarketSyncResponse, } from './protocol/market.ts';
|
|
16
16
|
export type { RepoSkillEntry, RepoRoot, RepoDiscoverResponse, RepoImportRequest, RepoImportResponse, RepoImportProgressResponse, RepoImportCancelRequest, RepoImportCancelResponse, UpdateCheckResponse, } from './protocol/repo.ts';
|
|
17
17
|
export type { SkillTag, CollectionGroup, GroupsResponse, TagSaveRequest, TagSaveResponse, TagDeleteRequest, TagDeleteResponse, TagMembersRequest, TagMembersResponse, TagReorderRequest, TagReorderResponse, CollectionReorderRequest, CollectionReorderResponse, SourceGroupReorderRequest, SourceGroupReorderResponse, } from './protocol/groups.ts';
|
|
18
18
|
export type { SourceRecord, TrashEntry, SourcesResponse, SourceCheckRequest, SourceCheckResult, SourceCheckResponse, SourceSyncRequest, SourceSyncResponse, SourceDeleteRequest, SourceDeleteResponse, SourceRestoreRequest, SourceRestoreResponse, SourceTrashClearResponse, } from './protocol/sources.ts';
|
|
@@ -30,6 +30,14 @@ export declare function githubAuthHeaders(): Record<string, string>;
|
|
|
30
30
|
export declare const NO_COMPRESSION: Record<string, string>;
|
|
31
31
|
/** JSON API 默认请求头(含鉴权)。 */
|
|
32
32
|
export declare function apiHeaders(): Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* When the response shows an exhausted GitHub rate limit, the instant it
|
|
35
|
+
* resets; otherwise undefined. Shared by the repo fetch layer and the plugin's
|
|
36
|
+
* own update check so both agree on what counts as a rate-limit rejection
|
|
37
|
+
* (403/429 + `x-ratelimit-remaining: 0` + a parseable `x-ratelimit-reset`)
|
|
38
|
+
* instead of each re-deriving it from the headers.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rateLimitResetAt(response: Response): Date | undefined;
|
|
33
41
|
/**
|
|
34
42
|
* Build a RepoFetchError; when the response shows an exhausted rate limit,
|
|
35
43
|
* report the reset time instead of a bare HTTP status.
|
|
@@ -46,10 +54,9 @@ export declare function fetchJson(url: string, fetchImpl: typeof fetch, context:
|
|
|
46
54
|
response: Response;
|
|
47
55
|
}>;
|
|
48
56
|
/**
|
|
49
|
-
* Load
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the exact same content a later download would fetch.
|
|
57
|
+
* Load one JSON document from the GitHub API, reusing an ETag when the URL was
|
|
58
|
+
* fetched before. `fetchImpl`/`context` exist so callers can stub the transport
|
|
59
|
+
* and shape the error text; the payload is whatever the endpoint returned.
|
|
53
60
|
*/
|
|
54
61
|
export declare function fetchJsonCached(url: string, fetchImpl: typeof fetch, context: string): Promise<{
|
|
55
62
|
json: unknown;
|
package/lib/types/repo.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The implementation now lives under `./repo/` (types, discovery, api,
|
|
13
13
|
* install) and this module is a barrel that re-exports it.
|
|
14
14
|
*/
|
|
15
|
-
export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, setGithubToken } from './repo/github-client.ts';
|
|
15
|
+
export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, rateLimitResetAt, setGithubToken } from './repo/github-client.ts';
|
|
16
16
|
export type { RepoRef, RepoTreeItem, RepoFile } from './repo/types.ts';
|
|
17
17
|
export { REPO_ROOT, isRepoRoot, repoSlug, normalizeRepoInput, collectRepoSkillFiles, originForRoot, discoverRepoEntries, skillManifest, skillDirOf, skillFileAt, skillPathIn, relativeToSkillDir, diffRemoteSkills, repoSkillEntry, } from './repo/discovery.ts';
|
|
18
18
|
export { loadRepoTree, getLatestReleaseTag, getRepoStats, listRepoReleases, listRepoBranches, getLatestCommit, loadRepoTreeAt, } from './repo/api.ts';
|
|
@@ -37,7 +37,12 @@ export interface SkillHubRouteDeps {
|
|
|
37
37
|
}
|
|
38
38
|
/** The resolved hub config a route sees (the shared resolver fills defaults). */
|
|
39
39
|
export declare function configOf(deps: SkillHubRouteDeps): HubConfig;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The raw saved config layer as persisted in the sidecar, reported for display
|
|
42
|
+
* only. Since dsh 0.1.7 the settings namespace is the one user layer, so this
|
|
43
|
+
* is legacy data: nothing writes it any more and the effective config above
|
|
44
|
+
* never merges it.
|
|
45
|
+
*/
|
|
41
46
|
export declare function savedOf(deps: SkillHubRouteDeps): Partial<HubConfig>;
|
|
42
47
|
/** Refuse business routes while the master switch is off (the config route stays up). */
|
|
43
48
|
export declare function disabledGate(deps: SkillHubRouteDeps, res: ServerResponse): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DiagnosticEntry, type WritableRoot } from '../protocol.ts';
|
|
2
2
|
/** One discoverable skill file in a scanned root. */
|
|
3
3
|
export interface SkillEntry {
|
|
4
4
|
/** Absolute path of the discovery file (SKILL.md or the flat .md). */
|
|
@@ -39,9 +39,10 @@ export declare function readSkillInterface(directory: string): Promise<SkillInte
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function findProjectRoot(cwd: string): Promise<string>;
|
|
41
41
|
/**
|
|
42
|
-
* Scan one writable root for files the provider ignores, so the GUI can
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Scan one writable root for files the provider ignores, so the GUI can show
|
|
43
|
+
* why a skill never appears — a missing frontmatter, a name that disagrees
|
|
44
|
+
* with the discovery path, and the like must be visible, not silent. Files the
|
|
45
|
+
* hub itself disabled (`.disabled`) are skipped: they are a toggle state, not
|
|
46
|
+
* a defect.
|
|
46
47
|
*/
|
|
47
48
|
export declare function scanDiagnostics(root: WritableRoot, home?: string): Promise<DiagnosticEntry[]>;
|
package/lib/types/skillfs.d.ts
CHANGED
|
@@ -29,6 +29,22 @@ export declare function createSkill(root: WritableRoot, name: string, descriptio
|
|
|
29
29
|
export declare function disableSkill(path: string): Promise<string>;
|
|
30
30
|
/** Re-enable a previously disabled file (reverse of disableSkill). */
|
|
31
31
|
export declare function enableSkill(disabledPath: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Move one already-resolved skill (a bundle directory or a flat .md file) into
|
|
34
|
+
* its root's trash as `<root>/.trash/<name>-<timestamp>`.
|
|
35
|
+
*
|
|
36
|
+
* The layout is the restore contract, not an implementation detail:
|
|
37
|
+
* `restoreSkill` renames the entry back and only proves the parent is `.trash`,
|
|
38
|
+
* and `clearTrash` deletes in place. Every producer of a trash entry must go
|
|
39
|
+
* through here — skill delete, the hub-disabled whole-group delete, and repo
|
|
40
|
+
* import's placeholder replacement all move skills for the same recovery flow.
|
|
41
|
+
*
|
|
42
|
+
* @returns the trashed path plus the original path that was moved.
|
|
43
|
+
*/
|
|
44
|
+
export declare function moveIntoTrash(source: string): Promise<{
|
|
45
|
+
path: string;
|
|
46
|
+
source: string;
|
|
47
|
+
}>;
|
|
32
48
|
/**
|
|
33
49
|
* Move a skill (directory bundle or flat .md file) into the root's trash.
|
|
34
50
|
* The skill stays recoverable: the source is renamed into
|
|
@@ -44,5 +60,13 @@ export declare function trashSkill(sourcePath: string): Promise<{
|
|
|
44
60
|
export declare function restoreSkill(entry: TrashEntry, home?: string): Promise<string>;
|
|
45
61
|
/** Permanently delete one trashed skill (directory or flat file). */
|
|
46
62
|
export declare function clearTrash(entry: TrashEntry, home?: string): Promise<string>;
|
|
47
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Repair one file on disk when its frontmatter is auto-fixable. Returns the new text.
|
|
65
|
+
*
|
|
66
|
+
* The write is atomic (sibling temp file + rename), matching the sidecar's
|
|
67
|
+
* persist path: this rewrites a real skill file in place, and a crash midway
|
|
68
|
+
* through a plain write would leave a truncated SKILL.md — exactly the failure
|
|
69
|
+
* the store layer already guards against. The `.tmp` sibling is never picked
|
|
70
|
+
* up by discovery (only `SKILL.md` and `*.md` are scanned).
|
|
71
|
+
*/
|
|
48
72
|
export declare function fixDiagnosticFile(path: string, home?: string): Promise<string>;
|