dsh-skill-hub 0.3.18 → 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 +14 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +187 -102
- package/lib/types/client/api.d.ts +3 -3
- package/lib/types/client/locales/market.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/protocol/config.d.ts +17 -11
- package/lib/types/protocol/market.d.ts +11 -0
- package/lib/types/protocol/repo.d.ts +6 -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/catalog-data.d.ts +11 -2
- package/lib/types/routes/deps.d.ts +6 -1
- package/lib/types/routes/route-state.d.ts +5 -0
- package/lib/types/skillfs/scan.d.ts +6 -5
- package/lib/types/skillfs.d.ts +25 -1
- package/package.json +22 -20
- package/src/client/api.ts +4 -3
- package/src/client/locales/market.ts +2 -0
- package/src/client/panel/RepoScanCard.tsx +2 -1
- package/src/client/panel/hooks/useMarketFlow.ts +4 -2
- package/src/client/panel/panel.module.css +10 -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/repo.ts +6 -0
- package/src/protocol.ts +1 -0
- package/src/repo/github-client.ts +26 -14
- package/src/repo/install.ts +5 -2
- package/src/repo.test.ts +40 -1
- package/src/repo.ts +1 -1
- package/src/routes/catalog-data.ts +20 -6
- 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 +49 -8
- package/src/routes/route-state.test.ts +1 -0
- package/src/routes/route-state.ts +5 -0
- package/src/routes.test.ts +187 -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 兜底)、整目录
|
|
@@ -2133,7 +2175,7 @@ async function downloadGitHubFile(repo, ref, path, fetchImpl = fetch, signal) {
|
|
|
2133
2175
|
}
|
|
2134
2176
|
if (response === null || !response.ok) {
|
|
2135
2177
|
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
2136
|
-
const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodedPath}`;
|
|
2178
|
+
const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`;
|
|
2137
2179
|
try {
|
|
2138
2180
|
response = await fetchImpl(apiUrl, {
|
|
2139
2181
|
headers: {
|
|
@@ -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;
|
|
@@ -2539,10 +2585,18 @@ async function checkLatestRelease(repo = UPDATE_REPO, fetchImpl = fetch) {
|
|
|
2539
2585
|
* routes 共享层 · 目录装配:已知工作区清单、完整目录响应(时间戳/interface
|
|
2540
2586
|
* 元数据/重名诊断)与详情映射。从 helpers.ts 原样搬出,行为不变。
|
|
2541
2587
|
*/
|
|
2542
|
-
/**
|
|
2543
|
-
|
|
2588
|
+
/**
|
|
2589
|
+
* 目录中存在的技能名集合。默认(`includeDisabled` 未显式关闭)为「启用目录 ∪
|
|
2590
|
+
* 已禁用名单」——tag 成员校验用:技能被禁用后不该从 tag 里掉出去。
|
|
2591
|
+
*
|
|
2592
|
+
* 导入去重必须传 `{ includeDisabled: false }`:禁用只意味着「暂不启用」,把它
|
|
2593
|
+
* 算作「已存在」会让一次禁用产生「永久占名」的副作用,真技能再也导不进来
|
|
2594
|
+
* (issue #16)。
|
|
2595
|
+
*/
|
|
2596
|
+
async function knownSkillNames(deps, options) {
|
|
2544
2597
|
const snapshot = await deps.skills.snapshot();
|
|
2545
2598
|
const names = new Set(snapshot.skills.map((skill) => skill.name));
|
|
2599
|
+
if (options?.includeDisabled === false) return names;
|
|
2546
2600
|
for (const disabled of await deps.store.listDisabled()) names.add(disabled.name);
|
|
2547
2601
|
return names;
|
|
2548
2602
|
}
|
|
@@ -2684,7 +2738,8 @@ async function buildCatalog(deps, cwd) {
|
|
|
2684
2738
|
if (iface !== void 0) applyInterface(row, iface);
|
|
2685
2739
|
return row;
|
|
2686
2740
|
});
|
|
2687
|
-
const
|
|
2741
|
+
const [userDshDiagnostics, userAgentsDiagnostics] = await Promise.all([scanDiagnostics("user-dsh", home), scanDiagnostics("user-agents", home)]);
|
|
2742
|
+
const diagnostics = [...userDshDiagnostics, ...userAgentsDiagnostics];
|
|
2688
2743
|
return {
|
|
2689
2744
|
ok: true,
|
|
2690
2745
|
pluginVersion: CURRENT_VERSION,
|
|
@@ -2873,15 +2928,7 @@ function catalogRoutes(deps) {
|
|
|
2873
2928
|
writeError(res, 409, "disabled skill path is outside the hub writable roots");
|
|
2874
2929
|
return;
|
|
2875
2930
|
}
|
|
2876
|
-
|
|
2877
|
-
const trashDir = join(dirname(sourceToTrash), ".trash");
|
|
2878
|
-
await mkdir(trashDir, { recursive: true });
|
|
2879
|
-
const target = join(trashDir, basename(sourceToTrash) + "-" + Date.now());
|
|
2880
|
-
await rename(sourceToTrash, target);
|
|
2881
|
-
trashResult = {
|
|
2882
|
-
path: target,
|
|
2883
|
-
source: sourceToTrash
|
|
2884
|
-
};
|
|
2931
|
+
trashResult = await moveIntoTrash(disabled.path.endsWith("SKILL.md.disabled") ? dirname(disabled.path) : disabled.path);
|
|
2885
2932
|
} else {
|
|
2886
2933
|
writeError(res, resolved.status, resolved.error);
|
|
2887
2934
|
return;
|
|
@@ -3295,6 +3342,7 @@ function marketRoutes(deps) {
|
|
|
3295
3342
|
}
|
|
3296
3343
|
try {
|
|
3297
3344
|
const latestTag = await getLatestReleaseTag(source.repo);
|
|
3345
|
+
lastMarketCheck.set(source.repo, now);
|
|
3298
3346
|
if (source.ref === void 0) {
|
|
3299
3347
|
results.push({
|
|
3300
3348
|
...base,
|
|
@@ -3305,7 +3353,6 @@ function marketRoutes(deps) {
|
|
|
3305
3353
|
continue;
|
|
3306
3354
|
}
|
|
3307
3355
|
const latest = await getLatestCommit(source.repo, source.ref);
|
|
3308
|
-
lastMarketCheck.set(source.repo, now);
|
|
3309
3356
|
const commitMoved = source.commitSha !== void 0 && latest.commitSha !== source.commitSha;
|
|
3310
3357
|
const newRelease = latestTag !== void 0 && latestTag !== source.ref;
|
|
3311
3358
|
results.push({
|
|
@@ -3350,9 +3397,11 @@ function marketRoutes(deps) {
|
|
|
3350
3397
|
return;
|
|
3351
3398
|
}
|
|
3352
3399
|
const ref = parsed.ref;
|
|
3400
|
+
const slug = repoSlug(parsed);
|
|
3353
3401
|
writeJson(res, 200, {
|
|
3354
3402
|
ok: true,
|
|
3355
|
-
|
|
3403
|
+
repo: slug,
|
|
3404
|
+
repos: await deps.store.addMarketSource(slug, ref)
|
|
3356
3405
|
});
|
|
3357
3406
|
}
|
|
3358
3407
|
},
|
|
@@ -3531,6 +3580,31 @@ function marketRoutes(deps) {
|
|
|
3531
3580
|
* 仓库导入域路由:repo 发现 / repoImport 异步导入任务 / progress 轮询 /
|
|
3532
3581
|
* cancel 取消。从 routes.ts 原样搬出,handler 逻辑不变。
|
|
3533
3582
|
*/
|
|
3583
|
+
/**
|
|
3584
|
+
* 导入去重只认「已启用」的目录,但磁盘上仍可能躺着一个已禁用的同名占位 ——
|
|
3585
|
+
* 禁用只是把 SKILL.md 改名成 SKILL.md.disabled,目录本身还在。这种占位不该
|
|
3586
|
+
* 堵住真技能:把它连同禁用记录一起移入回收站让位(issue #16)。
|
|
3587
|
+
*
|
|
3588
|
+
* 只有「禁用记录确实指向 `targetRoot/<name>` 的整目录禁用」才处理,且该目录
|
|
3589
|
+
* 必须已经没有 SKILL.md;其余不确定情形一律返回 false,交回原本的「跳过」。
|
|
3590
|
+
*/
|
|
3591
|
+
async function clearDisabledPlaceholder(deps, targetRoot, name) {
|
|
3592
|
+
const record = await deps.store.getDisabled(name);
|
|
3593
|
+
if (record === void 0) return false;
|
|
3594
|
+
const dir = join(targetRoot, name);
|
|
3595
|
+
const sourceToTrash = record.path.endsWith("SKILL.md.disabled") ? dirname(record.path) : record.path;
|
|
3596
|
+
if (sourceToTrash !== dir) return false;
|
|
3597
|
+
if (await pathExists(join(dir, "SKILL.md"))) return false;
|
|
3598
|
+
const { path: target } = await moveIntoTrash(sourceToTrash);
|
|
3599
|
+
await deps.store.addTrash({
|
|
3600
|
+
name,
|
|
3601
|
+
path: target,
|
|
3602
|
+
movedAt: Date.now(),
|
|
3603
|
+
sourcePath: sourceToTrash
|
|
3604
|
+
});
|
|
3605
|
+
await deps.store.removeDisabled(name);
|
|
3606
|
+
return true;
|
|
3607
|
+
}
|
|
3534
3608
|
/** 仓库导入域全部路由 spec(由 routes.ts 经 createRoute 包上统一围栏)。 */
|
|
3535
3609
|
function repoImportRoutes(deps) {
|
|
3536
3610
|
return [
|
|
@@ -3576,7 +3650,7 @@ function repoImportRoutes(deps) {
|
|
|
3576
3650
|
ok: true,
|
|
3577
3651
|
repo,
|
|
3578
3652
|
ref: resolvedRef,
|
|
3579
|
-
entries: discoverRepoEntries(tree, repo, await knownSkillNames(deps)),
|
|
3653
|
+
entries: discoverRepoEntries(tree, repo, await knownSkillNames(deps, { includeDisabled: false })),
|
|
3580
3654
|
...truncated ? { truncated: true } : {}
|
|
3581
3655
|
});
|
|
3582
3656
|
}
|
|
@@ -3600,7 +3674,7 @@ function repoImportRoutes(deps) {
|
|
|
3600
3674
|
const repo = repoSlug(parsed);
|
|
3601
3675
|
const requestedRef = readString(body, "ref");
|
|
3602
3676
|
const { ref: resolvedRef, tree } = await loadRepoTree(repo, requestedRef !== "" ? requestedRef : parsed.ref);
|
|
3603
|
-
const entries = discoverRepoEntries(tree, repo, await knownSkillNames(deps));
|
|
3677
|
+
const entries = discoverRepoEntries(tree, repo, await knownSkillNames(deps, { includeDisabled: false }));
|
|
3604
3678
|
const byPath = new Map(entries.map((entry) => [entry.path, entry]));
|
|
3605
3679
|
const selected = paths.map((path) => byPath.get(path)).filter((entry) => entry !== void 0);
|
|
3606
3680
|
if (selected.length === 0) {
|
|
@@ -3633,6 +3707,7 @@ function repoImportRoutes(deps) {
|
|
|
3633
3707
|
imported: [],
|
|
3634
3708
|
skipped: [],
|
|
3635
3709
|
failed: [],
|
|
3710
|
+
replaced: [],
|
|
3636
3711
|
status: "running",
|
|
3637
3712
|
controller,
|
|
3638
3713
|
createdAt: now
|
|
@@ -3659,13 +3734,22 @@ function repoImportRoutes(deps) {
|
|
|
3659
3734
|
continue;
|
|
3660
3735
|
}
|
|
3661
3736
|
if (await pathExists(join(targetRoot, entry.name))) {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3737
|
+
let cleared = false;
|
|
3738
|
+
try {
|
|
3739
|
+
cleared = await clearDisabledPlaceholder(deps, targetRoot, entry.name);
|
|
3740
|
+
} catch {
|
|
3741
|
+
cleared = false;
|
|
3742
|
+
}
|
|
3743
|
+
if (!cleared) {
|
|
3744
|
+
job.skipped.push({
|
|
3745
|
+
name: entry.name,
|
|
3746
|
+
reason: "exists"
|
|
3747
|
+
});
|
|
3748
|
+
job.downloadedBytes += entry.totalBytes;
|
|
3749
|
+
job.done += 1;
|
|
3750
|
+
continue;
|
|
3751
|
+
}
|
|
3752
|
+
job.replaced.push(entry.name);
|
|
3669
3753
|
}
|
|
3670
3754
|
if (controller.signal.aborted) break;
|
|
3671
3755
|
const files = collectRepoSkillFiles(tree, entry.dir);
|
|
@@ -3748,6 +3832,7 @@ function repoImportRoutes(deps) {
|
|
|
3748
3832
|
imported: [...job.imported],
|
|
3749
3833
|
skipped: [...job.skipped],
|
|
3750
3834
|
failed: [...job.failed],
|
|
3835
|
+
...job.replaced.length > 0 ? { replaced: [...job.replaced] } : {},
|
|
3751
3836
|
...job.error !== void 0 ? { error: job.error } : {}
|
|
3752
3837
|
});
|
|
3753
3838
|
}
|
|
@@ -4729,7 +4814,7 @@ function apply(ctx, config) {
|
|
|
4729
4814
|
}
|
|
4730
4815
|
return out;
|
|
4731
4816
|
};
|
|
4732
|
-
const current = () => resolveHubConfig(
|
|
4817
|
+
const current = () => resolveHubConfig(readConfig());
|
|
4733
4818
|
/** The Settings service, when this deployment mounts it. Absent ⇒ no user layer to read or write. */
|
|
4734
4819
|
const settingsOf = () => ctx.get("settings");
|
|
4735
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). */
|
|
@@ -70,6 +70,7 @@ export declare const zhMarket: {
|
|
|
70
70
|
readonly 'repo.files': "{count} 个文件 · {size}";
|
|
71
71
|
readonly 'repo.imported': "导入成功 {count} 个";
|
|
72
72
|
readonly 'repo.skippedExisting': "跳过已存在 {count} 个";
|
|
73
|
+
readonly 'repo.replaced': "已替换禁用占位 {count} 个(旧目录已移入回收站)";
|
|
73
74
|
readonly 'repo.failed': "失败 {count} 个";
|
|
74
75
|
readonly 'repo.cancel': "取消";
|
|
75
76
|
readonly 'repo.cancelled': "已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理";
|
|
@@ -153,6 +153,7 @@ export declare const zh: {
|
|
|
153
153
|
readonly 'repo.files': "{count} 个文件 · {size}";
|
|
154
154
|
readonly 'repo.imported': "导入成功 {count} 个";
|
|
155
155
|
readonly 'repo.skippedExisting': "跳过已存在 {count} 个";
|
|
156
|
+
readonly 'repo.replaced': "已替换禁用占位 {count} 个(旧目录已移入回收站)";
|
|
156
157
|
readonly 'repo.failed': "失败 {count} 个";
|
|
157
158
|
readonly 'repo.cancel': "取消";
|
|
158
159
|
readonly 'repo.cancelled': "已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理";
|
|
@@ -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;
|
|
@@ -96,6 +96,12 @@ export interface RepoImportProgressResponse {
|
|
|
96
96
|
name: string;
|
|
97
97
|
error: string;
|
|
98
98
|
}>;
|
|
99
|
+
/**
|
|
100
|
+
* 本次导入中「让位的已禁用同名占位」名字(issue #16):这些条目的旧目录已
|
|
101
|
+
* 移入回收站,新技能随后落地。旧目录挪走即计入,下载失败时也在列。老客户端
|
|
102
|
+
* 不认这个字段时忽略即可,故为可选。
|
|
103
|
+
*/
|
|
104
|
+
replaced?: string[];
|
|
99
105
|
/** error 状态时的原因 */
|
|
100
106
|
error?: string;
|
|
101
107
|
}
|
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';
|