dsh-skill-hub 0.3.13 → 0.3.15
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 +6 -4
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/lib/client.js +55 -58
- package/lib/client.js.map +1 -1
- package/lib/index.js +529 -315
- package/lib/types/client/SkillHubSettingsCard.d.ts +5 -3
- package/lib/types/client/icons.d.ts +4 -5
- package/lib/types/client/index.d.ts +13 -12
- package/lib/types/client/locales/market.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/settings-form.d.ts +9 -3
- package/lib/types/client/slash-dots.d.ts +20 -11
- package/lib/types/index.d.ts +59 -24
- package/lib/types/protocol/api.d.ts +22 -2
- package/lib/types/protocol/config.d.ts +30 -4
- package/lib/types/protocol/market.d.ts +1 -1
- package/lib/types/protocol/repo.d.ts +18 -1
- package/lib/types/protocol.d.ts +3 -3
- package/lib/types/repo/discovery.d.ts +59 -3
- package/lib/types/repo/github-client.d.ts +16 -0
- package/lib/types/repo.d.ts +6 -3
- package/lib/types/routes/helpers.d.ts +8 -1
- package/lib/types/routes/market.d.ts +2 -2
- package/lib/types/routes.d.ts +1 -1
- package/lib/types/store/store.d.ts +4 -0
- package/package.json +31 -41
- package/src/client/SkillHubSettingsCard.tsx +5 -3
- package/src/client/icons.tsx +4 -5
- package/src/client/index.tsx +33 -32
- package/src/client/locales/market.ts +2 -0
- package/src/client/panel/RepoScanCard.tsx +6 -3
- package/src/client/panel/SkillHubPanel.tsx +1 -1
- package/src/client/panel/SourcesView.tsx +1 -1
- package/src/client/panel/hooks/useGroupFlow.ts +1 -8
- package/src/client/settings-form.ts +9 -4
- package/src/client/slash-dots.test.ts +19 -36
- package/src/client/slash-dots.tsx +30 -60
- package/src/index.ts +145 -82
- package/src/protocol/api.ts +24 -2
- package/src/protocol/config.ts +35 -4
- package/src/protocol/market.ts +1 -1
- package/src/protocol/repo.ts +22 -1
- package/src/protocol.ts +3 -3
- package/src/repo/discovery.ts +120 -33
- package/src/repo/github-client.ts +18 -1
- package/src/repo/install.ts +8 -7
- package/src/repo.test.ts +158 -4
- package/src/repo.ts +10 -2
- package/src/routes/config.ts +19 -2
- package/src/routes/helpers.ts +9 -2
- package/src/routes/market.ts +49 -37
- package/src/routes/repo-import.ts +3 -2
- package/src/routes/sources.ts +2 -1
- package/src/routes.test.ts +107 -1
- package/src/routes.ts +21 -3
- package/src/store/migrate.ts +3 -2
- package/src/store/store.ts +7 -2
- package/src/store.test.ts +50 -0
- package/src/update.ts +4 -2
package/lib/index.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import z from "schemastery";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { dump, load } from "js-yaml";
|
|
7
6
|
import { isSkillName } from "@deepseek-ai/dsh-skill";
|
|
7
|
+
import { dump, load } from "js-yaml";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
//#region src/protocol/api.ts
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Root path of the skill-hub API family. The host also registers this as its
|
|
12
|
+
* 404 catch-all prefix, so a mistyped path answers with a plain 404 naming the
|
|
13
|
+
* path instead of falling through to the SPA fallback (which answers 401 and
|
|
14
|
+
* reads like an auth problem).
|
|
15
|
+
*/
|
|
16
|
+
const SKILL_HUB_API_ROOT = "/api/skill-hub";
|
|
17
|
+
/**
|
|
18
|
+
* Deprecated path of the market update check, kept routable for one release
|
|
19
|
+
* after the naming unification below. A browser tab that loaded the previous
|
|
20
|
+
* client bundle keeps calling it until it reloads; delete this (and its route
|
|
21
|
+
* in routes/market.ts) in the next minor.
|
|
22
|
+
*/
|
|
23
|
+
const SKILL_HUB_API_DEPRECATED_MARKET_CHECK = "/api/skill-hub/market/check";
|
|
24
|
+
/**
|
|
25
|
+
* Browser-facing base paths of the skill-hub API family.
|
|
26
|
+
*
|
|
27
|
+
* Naming rule: a path's segments mirror its scope. Market sources own the
|
|
28
|
+
* `/market/source/*` subtree — add, delete, ref, versions, check, sync — so
|
|
29
|
+
* the update check and the sync that acts on its result sit side by side.
|
|
30
|
+
*/
|
|
11
31
|
const SKILL_HUB_API = {
|
|
12
32
|
catalog: "/api/skill-hub/catalog",
|
|
13
33
|
skill: "/api/skill-hub/skill",
|
|
@@ -21,7 +41,7 @@ const SKILL_HUB_API = {
|
|
|
21
41
|
marketSource: "/api/skill-hub/market/source",
|
|
22
42
|
marketSourceDelete: "/api/skill-hub/market/source/delete",
|
|
23
43
|
marketSourceRef: "/api/skill-hub/market/source/ref",
|
|
24
|
-
marketCheck: "/api/skill-hub/market/check",
|
|
44
|
+
marketCheck: "/api/skill-hub/market/source/check",
|
|
25
45
|
marketSync: "/api/skill-hub/market/source/sync",
|
|
26
46
|
repo: "/api/skill-hub/repo",
|
|
27
47
|
repoImport: "/api/skill-hub/repo/import",
|
|
@@ -48,6 +68,14 @@ const SKILL_HUB_API = {
|
|
|
48
68
|
//#endregion
|
|
49
69
|
//#region src/protocol/config.ts
|
|
50
70
|
/**
|
|
71
|
+
* This plugin's Loader entry id — the settings namespace dsh serves its config
|
|
72
|
+
* under. It is cordis.patch.yml's insert id, NOT the package name, and it is
|
|
73
|
+
* shared by contract because the browser half addresses the same form through
|
|
74
|
+
* `ctx.configForms.get(HUB_ENTRY_ID)` while the host half writes through
|
|
75
|
+
* `ctx.settings.update(HUB_ENTRY_ID, …)`.
|
|
76
|
+
*/
|
|
77
|
+
const HUB_ENTRY_ID = "skill-hub";
|
|
78
|
+
/**
|
|
51
79
|
* Hub config defaults — the single source every layer reads: the cordis
|
|
52
80
|
* schema (index.ts), the host's saved-override merge, and the routes'
|
|
53
81
|
* fallback view. Changing a default here changes all three.
|
|
@@ -92,6 +120,16 @@ function clampNumber(value, min) {
|
|
|
92
120
|
if (typeof value !== "number" || !Number.isFinite(value) || value < min) return void 0;
|
|
93
121
|
return Math.floor(value);
|
|
94
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Drop the GitHub token from a config-shaped object. Returns a copy, so the
|
|
125
|
+
* caller's own config layer keeps the token. Shared by the config route's GET
|
|
126
|
+
* and POST responses — neither may echo it back.
|
|
127
|
+
*/
|
|
128
|
+
function redactGithubToken(value) {
|
|
129
|
+
const copy = { ...value };
|
|
130
|
+
delete copy.githubToken;
|
|
131
|
+
return copy;
|
|
132
|
+
}
|
|
95
133
|
/** HEX color validation shared by host routes and the settings card. */
|
|
96
134
|
const HEX_COLOR_RE = /^#[0-9a-f]{6}$/i;
|
|
97
135
|
/** GitHub token validation shared by host routes and the settings card. */
|
|
@@ -177,6 +215,18 @@ var StoreError = class extends Error {
|
|
|
177
215
|
}
|
|
178
216
|
};
|
|
179
217
|
//#endregion
|
|
218
|
+
//#region src/protocol/repo.ts
|
|
219
|
+
/** Top-level directory pattern for a skill root: visible, non-dot, safe chars. First char must be alphanum. */
|
|
220
|
+
const REPO_ROOT_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
221
|
+
/**
|
|
222
|
+
* True when a value is a usable root: a visible safe top-level directory name,
|
|
223
|
+
* or the empty string for the repo root. Guards the persisted store against
|
|
224
|
+
* corrupt roots without rewriting the repo-root sentinel.
|
|
225
|
+
*/
|
|
226
|
+
function isValidRepoRoot(value) {
|
|
227
|
+
return typeof value === "string" && (value === "" || REPO_ROOT_RE.test(value));
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
180
230
|
//#region src/store/migrate.ts
|
|
181
231
|
/**
|
|
182
232
|
* Normalize an arbitrary parsed sidecar document to the current schema.
|
|
@@ -269,7 +319,7 @@ function hydrateMigratedState(migrated) {
|
|
|
269
319
|
const source = entry;
|
|
270
320
|
if (source !== null && typeof source === "object" && typeof source.repo === "string" && source.repo !== "" && Array.isArray(source.skills)) {
|
|
271
321
|
const manifest = source.manifest;
|
|
272
|
-
const rawRoot =
|
|
322
|
+
const rawRoot = isValidRepoRoot(source.root) ? source.root : "skills";
|
|
273
323
|
sourcesByRepo.set(source.repo, {
|
|
274
324
|
repo: source.repo,
|
|
275
325
|
...typeof source.ref === "string" && source.ref !== "" ? { ref: source.ref } : {},
|
|
@@ -300,7 +350,7 @@ function hydrateMigratedState(migrated) {
|
|
|
300
350
|
path: item.path,
|
|
301
351
|
movedAt: typeof item.movedAt === "number" ? item.movedAt : 0,
|
|
302
352
|
...typeof item.sourcePath === "string" && item.sourcePath !== "" ? { sourcePath: item.sourcePath } : {},
|
|
303
|
-
...origin !== null && typeof origin === "object" && typeof origin.repo === "string" && origin.repo !== "" &&
|
|
353
|
+
...origin !== null && typeof origin === "object" && typeof origin.repo === "string" && origin.repo !== "" && isValidRepoRoot(origin.root) ? { origin: {
|
|
304
354
|
repo: origin.repo,
|
|
305
355
|
root: origin.root,
|
|
306
356
|
...typeof origin.ref === "string" && origin.ref !== "" ? { ref: origin.ref } : {},
|
|
@@ -393,6 +443,250 @@ function hydrateMigratedState(migrated) {
|
|
|
393
443
|
sourceGroupOrder
|
|
394
444
|
};
|
|
395
445
|
}
|
|
446
|
+
/** True when a root denotes the repo root itself (the repo is the skill dir). */
|
|
447
|
+
function isRepoRoot(root) {
|
|
448
|
+
return root === "";
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* The tree prefix delimiting a skill directory: empty for the repo-root skill
|
|
452
|
+
* (it owns the whole tree), otherwise `<dir>/`. Single source for collect,
|
|
453
|
+
* manifest, diff and the store's baseline cleanup, so they can never disagree.
|
|
454
|
+
*/
|
|
455
|
+
function skillDirPrefix(dir) {
|
|
456
|
+
return isRepoRoot(dir) ? "" : dir + "/";
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* A path inside a skill directory, as it appears in the repo tree. Both
|
|
460
|
+
* directions of this mapping matter: repo paths are always '/'-joined (never
|
|
461
|
+
* node:path's OS separator, these are GitHub paths shown in the UI), and the
|
|
462
|
+
* repo-root case must not grow a leading slash.
|
|
463
|
+
*/
|
|
464
|
+
function skillPathIn(dir, relative) {
|
|
465
|
+
return skillDirPrefix(dir) + relative;
|
|
466
|
+
}
|
|
467
|
+
/** The SKILL.md path inside a skill directory. */
|
|
468
|
+
function skillFileAt(dir) {
|
|
469
|
+
return skillPathIn(dir, "SKILL.md");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Top-level dot entries are repo tooling, never part of a skill: `.github/`,
|
|
473
|
+
* `.claude-plugin/`, `.gitignore`. Only the repo-root skill can reach them,
|
|
474
|
+
* since its prefix is empty and would otherwise sweep in the whole repo.
|
|
475
|
+
* Nested skill dirs cannot start with a dot (ROOT_RE), so this is a no-op
|
|
476
|
+
* for them.
|
|
477
|
+
*/
|
|
478
|
+
function isRepoTooling(path) {
|
|
479
|
+
return path.startsWith(".");
|
|
480
|
+
}
|
|
481
|
+
/** Every tree blob belonging to a skill directory, sorted by path. */
|
|
482
|
+
function skillTreeFiles(tree, dir) {
|
|
483
|
+
const prefix = skillDirPrefix(dir);
|
|
484
|
+
return tree.filter((item) => item.type === "blob" && item.path.startsWith(prefix) && !isRepoTooling(item.path)).map((item) => ({
|
|
485
|
+
path: item.path,
|
|
486
|
+
size: typeof item.size === "number" ? item.size : 0
|
|
487
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* A repo file's path relative to its skill directory — the path it is written
|
|
491
|
+
* to under the skill's own directory. `dir === ''` means the repo file path
|
|
492
|
+
* *is* the relative path; slicing `dir.length + 1` would eat its first
|
|
493
|
+
* character.
|
|
494
|
+
*/
|
|
495
|
+
function relativeToSkillDir(dir, path) {
|
|
496
|
+
return isRepoRoot(dir) ? path : path.slice(dir.length + 1);
|
|
497
|
+
}
|
|
498
|
+
/** `owner/repo` slug for a parsed reference. */
|
|
499
|
+
function repoSlug(ref) {
|
|
500
|
+
return `${ref.owner}/${ref.repo}`;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Normalize a GitHub URL or `owner/repo`/`owner/repo@ref` input.
|
|
504
|
+
* Only github.com URLs are accepted in v1.
|
|
505
|
+
*/
|
|
506
|
+
function normalizeRepoInput(input) {
|
|
507
|
+
let value = input.trim();
|
|
508
|
+
if (value === "") return null;
|
|
509
|
+
let ref;
|
|
510
|
+
const urlMatch = /^https?:\/\/github\.com\/([^/]+)\/([^/#?@]+)(?:\/tree\/([^/?#]+))?/.exec(value);
|
|
511
|
+
if (urlMatch !== null) {
|
|
512
|
+
value = `${urlMatch[1]}/${urlMatch[2]}`;
|
|
513
|
+
ref = urlMatch[3];
|
|
514
|
+
} else {
|
|
515
|
+
if (/^https?:\/\//.test(value)) return null;
|
|
516
|
+
const at = value.indexOf("@");
|
|
517
|
+
if (at !== -1) {
|
|
518
|
+
ref = value.slice(at + 1).trim();
|
|
519
|
+
value = value.slice(0, at).trim();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
value = value.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
523
|
+
const parts = value.split("/").filter(Boolean);
|
|
524
|
+
if (parts.length < 2) return null;
|
|
525
|
+
const owner = parts[0];
|
|
526
|
+
const repo = parts[1];
|
|
527
|
+
if (owner === "" || repo === "" || owner.includes("..") || repo.includes("..")) return null;
|
|
528
|
+
return {
|
|
529
|
+
owner,
|
|
530
|
+
repo,
|
|
531
|
+
...ref !== void 0 && ref !== "" ? { ref } : {}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** Collect every file inside a skill directory, including SKILL.md itself. */
|
|
535
|
+
function collectRepoSkillFiles(tree, dir) {
|
|
536
|
+
return skillTreeFiles(tree, dir);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Compute an origin collection name. Multiple roots split by root, one root
|
|
540
|
+
* keeps the repo slug. The repo-root skill's name *is* the repo slug, so it
|
|
541
|
+
* never gets a trailing-slash suffix (`owner/repo/`).
|
|
542
|
+
*/
|
|
543
|
+
function originForRoot(repo, rootsPresent, root) {
|
|
544
|
+
if (rootsPresent.size <= 1) return repo;
|
|
545
|
+
return isRepoRoot(root) ? repo : `${repo}/${root}`;
|
|
546
|
+
}
|
|
547
|
+
/** Discover importable skills from a repo tree. Invalid names are ignored. Roots are auto-derived from the top-level directory of each SKILL.md. */
|
|
548
|
+
function discoverRepoEntries(tree, repo, existingNames = /* @__PURE__ */ new Set()) {
|
|
549
|
+
const candidates = [];
|
|
550
|
+
const rootsPresent = /* @__PURE__ */ new Set();
|
|
551
|
+
const repoName = repo.slice(repo.lastIndexOf("/") + 1);
|
|
552
|
+
if (isSkillName(repoName) && tree.some((item) => item.type === "blob" && item.path === "SKILL.md")) {
|
|
553
|
+
rootsPresent.add("");
|
|
554
|
+
candidates.push({
|
|
555
|
+
root: "",
|
|
556
|
+
dir: "",
|
|
557
|
+
name: repoName,
|
|
558
|
+
path: "SKILL.md"
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
for (const item of tree) {
|
|
562
|
+
if (item.type !== "blob") continue;
|
|
563
|
+
const slash = item.path.indexOf("/");
|
|
564
|
+
if (slash === -1) continue;
|
|
565
|
+
if (!item.path.endsWith("/SKILL.md")) continue;
|
|
566
|
+
const root = item.path.slice(0, slash);
|
|
567
|
+
if (!REPO_ROOT_RE.test(root)) continue;
|
|
568
|
+
const dir = item.path.slice(0, -9);
|
|
569
|
+
if (dir === root || dir.length <= root.length + 1) continue;
|
|
570
|
+
const name = dir.slice(dir.lastIndexOf("/") + 1);
|
|
571
|
+
if (!isSkillName(name)) continue;
|
|
572
|
+
rootsPresent.add(root);
|
|
573
|
+
candidates.push({
|
|
574
|
+
root,
|
|
575
|
+
dir,
|
|
576
|
+
name,
|
|
577
|
+
path: item.path
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return candidates.map((candidate) => {
|
|
581
|
+
const files = collectRepoSkillFiles(tree, candidate.dir);
|
|
582
|
+
return {
|
|
583
|
+
name: candidate.name,
|
|
584
|
+
dir: candidate.dir,
|
|
585
|
+
path: candidate.path,
|
|
586
|
+
root: candidate.root,
|
|
587
|
+
origin: originForRoot(repo, rootsPresent, candidate.root),
|
|
588
|
+
fileCount: files.length,
|
|
589
|
+
totalBytes: files.reduce((sum, file) => sum + file.size, 0),
|
|
590
|
+
existing: existingNames.has(candidate.name)
|
|
591
|
+
};
|
|
592
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Build a path→size manifest for one skill directory from a repo tree.
|
|
596
|
+
* Shares `skillTreeFiles` with collectRepoSkillFiles: a manifest that
|
|
597
|
+
* disagreed with the collected file set would make every update diff report
|
|
598
|
+
* "changed" forever.
|
|
599
|
+
*/
|
|
600
|
+
function skillManifest(tree, dir) {
|
|
601
|
+
const manifest = {};
|
|
602
|
+
for (const file of skillTreeFiles(tree, dir)) manifest[file.path] = file.size;
|
|
603
|
+
return manifest;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* The real upstream directory of one tracked skill. Source records keep only
|
|
607
|
+
* the short name and top-level root (e.g. root "skills", name "ask-matt"),
|
|
608
|
+
* but upstream repos may nest skills under category directories
|
|
609
|
+
* (skills/engineering/ask-matt/) or move them between categories. Resolution
|
|
610
|
+
* order: exact match under the top-level root in the upstream tree, then the
|
|
611
|
+
* manifest's recorded blob path, then any same-name skill in the tree, then
|
|
612
|
+
* the flat root/name fallback. Passing the tree paths makes the lookup
|
|
613
|
+
* resilient to incomplete manifests and upstream moves.
|
|
614
|
+
*
|
|
615
|
+
* A repo-root record (root '') has no directory to resolve: its SKILL.md is a
|
|
616
|
+
* bare top-level path, so every nested lookup below would miss and the final
|
|
617
|
+
* fallback would hand back a bogus '/name'. Answer before them — including
|
|
618
|
+
* when the upstream SKILL.md is gone, so diffRemoteSkills reports a deletion
|
|
619
|
+
* instead of hunting for a skill that was never nested.
|
|
620
|
+
*/
|
|
621
|
+
function skillDirOf(source, name, treePaths) {
|
|
622
|
+
if (isRepoRoot(source.root)) return "";
|
|
623
|
+
if (treePaths !== void 0) {
|
|
624
|
+
const rootPrefix = source.root + "/";
|
|
625
|
+
const found = treePaths.find((path) => path.startsWith(rootPrefix) && path.endsWith("/" + name + "/SKILL.md"));
|
|
626
|
+
if (found !== void 0) return found.slice(0, found.lastIndexOf("/"));
|
|
627
|
+
}
|
|
628
|
+
const manifest = source.manifest ?? {};
|
|
629
|
+
const fromManifest = Object.keys(manifest).find((path) => path.endsWith("/" + name + "/SKILL.md"));
|
|
630
|
+
if (fromManifest !== void 0) return fromManifest.slice(0, fromManifest.lastIndexOf("/"));
|
|
631
|
+
if (treePaths !== void 0) {
|
|
632
|
+
const found = treePaths.find((path) => path.endsWith("/" + name + "/SKILL.md"));
|
|
633
|
+
if (found !== void 0) return found.slice(0, found.lastIndexOf("/"));
|
|
634
|
+
}
|
|
635
|
+
return source.root + "/" + name;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Diff one source record against an upstream tree at treeSha: which tracked
|
|
639
|
+
* skills disappeared (no SKILL.md blob) and which changed (manifest baseline
|
|
640
|
+
* differs, or no baseline exists — treated as changed). Pure over the tree.
|
|
641
|
+
* The remote side is read through skillManifest, so the diff compares like
|
|
642
|
+
* with like against the baseline recorded at import time.
|
|
643
|
+
*/
|
|
644
|
+
function diffRemoteSkills(tree, source) {
|
|
645
|
+
const treePaths = tree.filter((item) => item.type === "blob").map((item) => item.path);
|
|
646
|
+
const updated = [];
|
|
647
|
+
const deleted = [];
|
|
648
|
+
const baseline = source.manifest ?? {};
|
|
649
|
+
for (const name of source.skills) {
|
|
650
|
+
const dir = skillDirOf(source, name, treePaths);
|
|
651
|
+
const prefix = skillDirPrefix(dir);
|
|
652
|
+
const remote = skillManifest(tree, dir);
|
|
653
|
+
if (!Object.hasOwn(remote, skillFileAt(dir))) {
|
|
654
|
+
deleted.push(name);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const baselineEntries = Object.entries(baseline).filter(([path]) => path.startsWith(prefix));
|
|
658
|
+
if (baselineEntries.length === 0) {
|
|
659
|
+
updated.push(name);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
let differs = baselineEntries.length !== Object.keys(remote).length;
|
|
663
|
+
if (!differs) {
|
|
664
|
+
for (const [path, size] of baselineEntries) if (remote[path] !== size) {
|
|
665
|
+
differs = true;
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (differs) updated.push(name);
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
updated,
|
|
673
|
+
deleted
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** Minimal RepoSkillEntry for a tracked skill name (sync re-downloads by name). */
|
|
677
|
+
function repoSkillEntry(name, root, repo) {
|
|
678
|
+
const dir = isRepoRoot(root) ? "" : root + "/" + name;
|
|
679
|
+
return {
|
|
680
|
+
name,
|
|
681
|
+
dir,
|
|
682
|
+
path: skillFileAt(dir),
|
|
683
|
+
root,
|
|
684
|
+
origin: repo,
|
|
685
|
+
fileCount: 0,
|
|
686
|
+
totalBytes: 0,
|
|
687
|
+
existing: false
|
|
688
|
+
};
|
|
689
|
+
}
|
|
396
690
|
//#endregion
|
|
397
691
|
//#region src/store/store.ts
|
|
398
692
|
/** Sidecar state owner. */
|
|
@@ -725,14 +1019,18 @@ var SkillHubStore = class {
|
|
|
725
1019
|
* When `dir` is given, every baseline path under that skill directory is
|
|
726
1020
|
* dropped first, so files the upstream removed never linger in the
|
|
727
1021
|
* baseline and skew later update diffs.
|
|
1022
|
+
*
|
|
1023
|
+
* `dir` may be '' — a skill whose SKILL.md sits at the repo root. Its prefix
|
|
1024
|
+
* is empty, so the whole baseline is replaced: that skill owns the tree, and
|
|
1025
|
+
* keeping stale paths would make every later diff report "changed" forever.
|
|
728
1026
|
*/
|
|
729
1027
|
async mergeSourceManifest(repo, manifest, dir) {
|
|
730
1028
|
await this.ensureLoaded();
|
|
731
1029
|
const existing = this.sourcesByRepo.get(repo);
|
|
732
1030
|
if (existing === void 0 || Object.keys(manifest).length === 0) return;
|
|
733
1031
|
const base = { ...existing.manifest ?? {} };
|
|
734
|
-
if (dir !== void 0
|
|
735
|
-
const prefix = dir
|
|
1032
|
+
if (dir !== void 0) {
|
|
1033
|
+
const prefix = skillDirPrefix(dir);
|
|
736
1034
|
for (const path of Object.keys(base)) if (path.startsWith(prefix)) delete base[path];
|
|
737
1035
|
}
|
|
738
1036
|
this.sourcesByRepo.set(repo, {
|
|
@@ -1584,10 +1882,27 @@ function setGithubToken(token) {
|
|
|
1584
1882
|
function githubAuthHeaders() {
|
|
1585
1883
|
return githubToken === "" ? {} : { authorization: "Bearer " + githubToken };
|
|
1586
1884
|
}
|
|
1885
|
+
/**
|
|
1886
|
+
* 传输层:显式要求服务端返回未压缩实体。
|
|
1887
|
+
*
|
|
1888
|
+
* 为什么必须加:dsh 会把**启动环境**里的 `HTTP_PROXY`/`HTTPS_PROXY` 装成
|
|
1889
|
+
* undici 的全局 dispatcher(`@deepseek-ai/dsh-http-proxy`),插件里的 `fetch`
|
|
1890
|
+
* 因此走该代理。实测本机 Clash:经代理返回的响应会**丢掉 `content-encoding`
|
|
1891
|
+
* 与 `content-type` 头,而 body 仍是 gzip**;undici 只依据 `content-encoding`
|
|
1892
|
+
* 决定是否解压,于是 `response.json()` 拿到 gzip 二进制并抛错,被上层的
|
|
1893
|
+
* catch 映射成 `invalid github response for <url>` —— 看起来像 GitHub 坏了。
|
|
1894
|
+
* 实测对照(同一 URL、同一代理):默认 1331 字节解析失败,声明 identity 后
|
|
1895
|
+
* 5245 字节解析成功。
|
|
1896
|
+
*
|
|
1897
|
+
* 代价是不走压缩(我们的响应体都很小),换来的是**无论中间代理是否改写头部
|
|
1898
|
+
* 都能正确取到实体**,且用户无需为插件调整启动命令。
|
|
1899
|
+
*/
|
|
1900
|
+
const NO_COMPRESSION = { "accept-encoding": "identity" };
|
|
1587
1901
|
/** JSON API 默认请求头(含鉴权)。 */
|
|
1588
1902
|
function apiHeaders() {
|
|
1589
1903
|
return {
|
|
1590
1904
|
accept: "application/vnd.github+json",
|
|
1905
|
+
...NO_COMPRESSION,
|
|
1591
1906
|
...githubAuthHeaders()
|
|
1592
1907
|
};
|
|
1593
1908
|
}
|
|
@@ -1677,184 +1992,6 @@ async function fetchJsonCached(url, fetchImpl, context) {
|
|
|
1677
1992
|
};
|
|
1678
1993
|
}
|
|
1679
1994
|
//#endregion
|
|
1680
|
-
//#region src/repo/discovery.ts
|
|
1681
|
-
/**
|
|
1682
|
-
* 仓库技能发现:从 GitHub 仓库树推断技能根目录、列出可导入技能、
|
|
1683
|
-
* 生成目录清单并做上游差异比对。纯函数为主,便于单测。
|
|
1684
|
-
* 从 repo.ts 抽出。
|
|
1685
|
-
*/
|
|
1686
|
-
/** Top-level directory pattern for a skill root: visible, non-dot, safe chars. First char must be alphanum. */
|
|
1687
|
-
const ROOT_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
1688
|
-
/** `owner/repo` slug for a parsed reference. */
|
|
1689
|
-
function repoSlug(ref) {
|
|
1690
|
-
return `${ref.owner}/${ref.repo}`;
|
|
1691
|
-
}
|
|
1692
|
-
/**
|
|
1693
|
-
* Normalize a GitHub URL or `owner/repo`/`owner/repo@ref` input.
|
|
1694
|
-
* Only github.com URLs are accepted in v1.
|
|
1695
|
-
*/
|
|
1696
|
-
function normalizeRepoInput(input) {
|
|
1697
|
-
let value = input.trim();
|
|
1698
|
-
if (value === "") return null;
|
|
1699
|
-
let ref;
|
|
1700
|
-
const urlMatch = /^https?:\/\/github\.com\/([^/]+)\/([^/#?@]+)(?:\/tree\/([^/?#]+))?/.exec(value);
|
|
1701
|
-
if (urlMatch !== null) {
|
|
1702
|
-
value = `${urlMatch[1]}/${urlMatch[2]}`;
|
|
1703
|
-
ref = urlMatch[3];
|
|
1704
|
-
} else {
|
|
1705
|
-
if (/^https?:\/\//.test(value)) return null;
|
|
1706
|
-
const at = value.indexOf("@");
|
|
1707
|
-
if (at !== -1) {
|
|
1708
|
-
ref = value.slice(at + 1).trim();
|
|
1709
|
-
value = value.slice(0, at).trim();
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
value = value.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
1713
|
-
const parts = value.split("/").filter(Boolean);
|
|
1714
|
-
if (parts.length < 2) return null;
|
|
1715
|
-
const owner = parts[0];
|
|
1716
|
-
const repo = parts[1];
|
|
1717
|
-
if (owner === "" || repo === "" || owner.includes("..") || repo.includes("..")) return null;
|
|
1718
|
-
return {
|
|
1719
|
-
owner,
|
|
1720
|
-
repo,
|
|
1721
|
-
...ref !== void 0 && ref !== "" ? { ref } : {}
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
/** Collect every file inside a skill directory, including SKILL.md itself. */
|
|
1725
|
-
function collectRepoSkillFiles(tree, dir) {
|
|
1726
|
-
const prefix = dir + "/";
|
|
1727
|
-
return tree.filter((item) => item.type === "blob" && item.path.startsWith(prefix)).map((item) => ({
|
|
1728
|
-
path: item.path,
|
|
1729
|
-
size: typeof item.size === "number" ? item.size : 0
|
|
1730
|
-
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
1731
|
-
}
|
|
1732
|
-
/** Compute an origin collection name. Multiple roots split by root, one root keeps the repo slug. */
|
|
1733
|
-
function originForRoot(repo, rootsPresent, root) {
|
|
1734
|
-
return rootsPresent.size > 1 ? `${repo}/${root}` : repo;
|
|
1735
|
-
}
|
|
1736
|
-
/** Discover importable skills from a repo tree. Invalid names are ignored. Roots are auto-derived from the top-level directory of each SKILL.md. */
|
|
1737
|
-
function discoverRepoEntries(tree, repo, existingNames = /* @__PURE__ */ new Set()) {
|
|
1738
|
-
const candidates = [];
|
|
1739
|
-
const rootsPresent = /* @__PURE__ */ new Set();
|
|
1740
|
-
for (const item of tree) {
|
|
1741
|
-
if (item.type !== "blob") continue;
|
|
1742
|
-
const slash = item.path.indexOf("/");
|
|
1743
|
-
if (slash === -1) continue;
|
|
1744
|
-
if (!item.path.endsWith("/SKILL.md")) continue;
|
|
1745
|
-
const root = item.path.slice(0, slash);
|
|
1746
|
-
if (!ROOT_RE.test(root)) continue;
|
|
1747
|
-
const dir = item.path.slice(0, -9);
|
|
1748
|
-
if (dir === root || dir.length <= root.length + 1) continue;
|
|
1749
|
-
const name = dir.slice(dir.lastIndexOf("/") + 1);
|
|
1750
|
-
if (!isSkillName(name)) continue;
|
|
1751
|
-
rootsPresent.add(root);
|
|
1752
|
-
candidates.push({
|
|
1753
|
-
root,
|
|
1754
|
-
dir,
|
|
1755
|
-
name,
|
|
1756
|
-
path: item.path
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
return candidates.map((candidate) => {
|
|
1760
|
-
const files = collectRepoSkillFiles(tree, candidate.dir);
|
|
1761
|
-
return {
|
|
1762
|
-
name: candidate.name,
|
|
1763
|
-
dir: candidate.dir,
|
|
1764
|
-
path: candidate.path,
|
|
1765
|
-
root: candidate.root,
|
|
1766
|
-
origin: originForRoot(repo, rootsPresent, candidate.root),
|
|
1767
|
-
fileCount: files.length,
|
|
1768
|
-
totalBytes: files.reduce((sum, file) => sum + file.size, 0),
|
|
1769
|
-
existing: existingNames.has(candidate.name)
|
|
1770
|
-
};
|
|
1771
|
-
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1772
|
-
}
|
|
1773
|
-
/** Build a path→size manifest for one skill directory from a repo tree. */
|
|
1774
|
-
function skillManifest(tree, dir) {
|
|
1775
|
-
const prefix = dir + "/";
|
|
1776
|
-
const manifest = {};
|
|
1777
|
-
for (const item of tree) if (item.type === "blob" && item.path.startsWith(prefix)) manifest[item.path] = typeof item.size === "number" ? item.size : 0;
|
|
1778
|
-
return manifest;
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* The real upstream directory of one tracked skill. Source records keep only
|
|
1782
|
-
* the short name and top-level root (e.g. root "skills", name "ask-matt"),
|
|
1783
|
-
* but upstream repos may nest skills under category directories
|
|
1784
|
-
* (skills/engineering/ask-matt/) or move them between categories. Resolution
|
|
1785
|
-
* order: exact match under the top-level root in the upstream tree, then the
|
|
1786
|
-
* manifest's recorded blob path, then any same-name skill in the tree, then
|
|
1787
|
-
* the flat root/name fallback. Passing the tree paths makes the lookup
|
|
1788
|
-
* resilient to incomplete manifests and upstream moves.
|
|
1789
|
-
*/
|
|
1790
|
-
function skillDirOf(source, name, treePaths) {
|
|
1791
|
-
if (treePaths !== void 0) {
|
|
1792
|
-
const rootPrefix = source.root + "/";
|
|
1793
|
-
const found = treePaths.find((path) => path.startsWith(rootPrefix) && path.endsWith("/" + name + "/SKILL.md"));
|
|
1794
|
-
if (found !== void 0) return found.slice(0, found.lastIndexOf("/"));
|
|
1795
|
-
}
|
|
1796
|
-
const manifest = source.manifest ?? {};
|
|
1797
|
-
const fromManifest = Object.keys(manifest).find((path) => path.endsWith("/" + name + "/SKILL.md"));
|
|
1798
|
-
if (fromManifest !== void 0) return fromManifest.slice(0, fromManifest.lastIndexOf("/"));
|
|
1799
|
-
if (treePaths !== void 0) {
|
|
1800
|
-
const found = treePaths.find((path) => path.endsWith("/" + name + "/SKILL.md"));
|
|
1801
|
-
if (found !== void 0) return found.slice(0, found.lastIndexOf("/"));
|
|
1802
|
-
}
|
|
1803
|
-
return source.root + "/" + name;
|
|
1804
|
-
}
|
|
1805
|
-
/**
|
|
1806
|
-
* Diff one source record against an upstream tree at treeSha: which tracked
|
|
1807
|
-
* skills disappeared (no SKILL.md blob) and which changed (manifest baseline
|
|
1808
|
-
* differs, or no baseline exists — treated as changed). Pure over the tree.
|
|
1809
|
-
*/
|
|
1810
|
-
function diffRemoteSkills(tree, source) {
|
|
1811
|
-
const blobs = /* @__PURE__ */ new Map();
|
|
1812
|
-
for (const item of tree) if (item.type === "blob") blobs.set(item.path, typeof item.size === "number" ? item.size : 0);
|
|
1813
|
-
const treePaths = [...blobs.keys()];
|
|
1814
|
-
const updated = [];
|
|
1815
|
-
const deleted = [];
|
|
1816
|
-
for (const name of source.skills) {
|
|
1817
|
-
const prefix = skillDirOf(source, name, treePaths) + "/";
|
|
1818
|
-
const remote = /* @__PURE__ */ new Map();
|
|
1819
|
-
for (const [path, size] of blobs) if (path.startsWith(prefix)) remote.set(path, size);
|
|
1820
|
-
if (!remote.has(prefix + "SKILL.md")) {
|
|
1821
|
-
deleted.push(name);
|
|
1822
|
-
continue;
|
|
1823
|
-
}
|
|
1824
|
-
const baseline = source.manifest ?? {};
|
|
1825
|
-
const baselineEntries = Object.entries(baseline).filter(([path]) => path.startsWith(prefix));
|
|
1826
|
-
if (baselineEntries.length === 0) {
|
|
1827
|
-
updated.push(name);
|
|
1828
|
-
continue;
|
|
1829
|
-
}
|
|
1830
|
-
let differs = baselineEntries.length !== remote.size;
|
|
1831
|
-
if (!differs) {
|
|
1832
|
-
for (const [path, size] of baselineEntries) if (remote.get(path) !== size) {
|
|
1833
|
-
differs = true;
|
|
1834
|
-
break;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
if (differs) updated.push(name);
|
|
1838
|
-
}
|
|
1839
|
-
return {
|
|
1840
|
-
updated,
|
|
1841
|
-
deleted
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
/** Minimal RepoSkillEntry for a tracked skill name (sync re-downloads by name). */
|
|
1845
|
-
function repoSkillEntry(name, root, repo) {
|
|
1846
|
-
return {
|
|
1847
|
-
name,
|
|
1848
|
-
dir: root + "/" + name,
|
|
1849
|
-
path: root + "/" + name + "/SKILL.md",
|
|
1850
|
-
root,
|
|
1851
|
-
origin: repo,
|
|
1852
|
-
fileCount: 0,
|
|
1853
|
-
totalBytes: 0,
|
|
1854
|
-
existing: false
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
//#endregion
|
|
1858
1995
|
//#region src/repo/api.ts
|
|
1859
1996
|
/**
|
|
1860
1997
|
* GitHub 仓库读取:仓库树、发布标签、星标/下载量、分支与提交查询。
|
|
@@ -1991,7 +2128,10 @@ async function downloadGitHubFile(repo, ref, path, fetchImpl = fetch, signal) {
|
|
|
1991
2128
|
let response = null;
|
|
1992
2129
|
try {
|
|
1993
2130
|
response = await fetchImpl(rawUrl, {
|
|
1994
|
-
headers:
|
|
2131
|
+
headers: {
|
|
2132
|
+
...NO_COMPRESSION,
|
|
2133
|
+
...githubAuthHeaders()
|
|
2134
|
+
},
|
|
1995
2135
|
...signal !== void 0 ? { signal } : {}
|
|
1996
2136
|
});
|
|
1997
2137
|
} catch (error) {
|
|
@@ -2005,6 +2145,7 @@ async function downloadGitHubFile(repo, ref, path, fetchImpl = fetch, signal) {
|
|
|
2005
2145
|
response = await fetchImpl(apiUrl, {
|
|
2006
2146
|
headers: {
|
|
2007
2147
|
accept: "application/vnd.github.raw",
|
|
2148
|
+
...NO_COMPRESSION,
|
|
2008
2149
|
...githubAuthHeaders()
|
|
2009
2150
|
},
|
|
2010
2151
|
...signal !== void 0 ? { signal } : {}
|
|
@@ -2036,7 +2177,7 @@ async function downloadRepoSkill(repo, ref, entry, files, targetRoot, fetchImpl
|
|
|
2036
2177
|
try {
|
|
2037
2178
|
await mapConcurrent(files, 6, async (file) => {
|
|
2038
2179
|
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
2039
|
-
const relative =
|
|
2180
|
+
const relative = relativeToSkillDir(entry.dir, file.path);
|
|
2040
2181
|
if (relative === "" || relative.includes("..")) throw new RepoFetchError("unsafe repo path: " + file.path);
|
|
2041
2182
|
const target = join(tempDir, relative);
|
|
2042
2183
|
const buffer = await downloadGitHubFile(repo, ref, file.path, fetchImpl, signal);
|
|
@@ -2074,7 +2215,7 @@ async function downloadRepoSkill(repo, ref, entry, files, targetRoot, fetchImpl
|
|
|
2074
2215
|
force: true
|
|
2075
2216
|
});
|
|
2076
2217
|
} catch (secondError) {
|
|
2077
|
-
console.warn(`[skill-hub] cleanup tempDir failed ${tempDir}:`, errorText(secondError), "first:", errorText(firstError));
|
|
2218
|
+
console.warn(`[dsh-skill-hub] cleanup tempDir failed ${tempDir}:`, errorText(secondError), "first:", errorText(firstError));
|
|
2078
2219
|
}
|
|
2079
2220
|
}
|
|
2080
2221
|
}
|
|
@@ -2102,10 +2243,10 @@ async function cleanupLeftoverImportDirs(targetRoot) {
|
|
|
2102
2243
|
});
|
|
2103
2244
|
cleaned += 1;
|
|
2104
2245
|
} catch (error) {
|
|
2105
|
-
console.warn(`[skill-hub] startup cleanup failed ${full}:`, errorText(error));
|
|
2246
|
+
console.warn(`[dsh-skill-hub] startup cleanup failed ${full}:`, errorText(error));
|
|
2106
2247
|
}
|
|
2107
2248
|
}
|
|
2108
|
-
if (cleaned > 0) console.warn(`[skill-hub] startup cleaned ${cleaned} leftover import temp dir(s) in ${targetRoot}`);
|
|
2249
|
+
if (cleaned > 0) console.warn(`[dsh-skill-hub] startup cleaned ${cleaned} leftover import temp dir(s) in ${targetRoot}`);
|
|
2109
2250
|
return cleaned;
|
|
2110
2251
|
}
|
|
2111
2252
|
/** Loopback literal check plus browser same-origin markers. */
|
|
@@ -2362,10 +2503,7 @@ async function checkLatestRelease(repo = UPDATE_REPO, fetchImpl = fetch) {
|
|
|
2362
2503
|
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
2363
2504
|
let response;
|
|
2364
2505
|
try {
|
|
2365
|
-
response = await fetchImpl(url, { headers:
|
|
2366
|
-
accept: "application/vnd.github+json",
|
|
2367
|
-
...githubAuthHeaders()
|
|
2368
|
-
} });
|
|
2506
|
+
response = await fetchImpl(url, { headers: apiHeaders() });
|
|
2369
2507
|
} catch (error) {
|
|
2370
2508
|
return failure(errorText(error));
|
|
2371
2509
|
}
|
|
@@ -2598,7 +2736,7 @@ function toDetail(skill) {
|
|
|
2598
2736
|
*/
|
|
2599
2737
|
function createRoute(deps, spec) {
|
|
2600
2738
|
return {
|
|
2601
|
-
kind: "exact",
|
|
2739
|
+
kind: spec.kind ?? "exact",
|
|
2602
2740
|
path: spec.path,
|
|
2603
2741
|
handler: async (req, res) => {
|
|
2604
2742
|
if (!isLoopbackRequest(req)) {
|
|
@@ -2962,6 +3100,21 @@ function catalogRoutes(deps) {
|
|
|
2962
3100
|
* 配置域路由:config(总开关开着也可用,设置卡自救入口)/ update 自身更新
|
|
2963
3101
|
* 检查 / diagnosticFix。从 routes.ts 原样搬出,handler 逻辑不变。
|
|
2964
3102
|
*/
|
|
3103
|
+
/**
|
|
3104
|
+
* Build a config payload for either method. The GitHub token never goes out:
|
|
3105
|
+
* the effective config and the raw saved overrides are both stripped, and its
|
|
3106
|
+
* presence is reported as a plain boolean instead.
|
|
3107
|
+
*/
|
|
3108
|
+
function configResponse(config, saved) {
|
|
3109
|
+
const token = config.githubToken;
|
|
3110
|
+
return {
|
|
3111
|
+
ok: true,
|
|
3112
|
+
pluginVersion: CURRENT_VERSION,
|
|
3113
|
+
config: redactGithubToken(config),
|
|
3114
|
+
saved: redactGithubToken(saved),
|
|
3115
|
+
githubTokenSet: token !== void 0 && token !== ""
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
2965
3118
|
/** 配置域全部路由 spec(由 routes.ts 经 createRoute 包上统一围栏)。 */
|
|
2966
3119
|
function configRoutes(deps) {
|
|
2967
3120
|
return [
|
|
@@ -2972,12 +3125,7 @@ function configRoutes(deps) {
|
|
|
2972
3125
|
skipGate: true,
|
|
2973
3126
|
handler: async ({ req, res, body }) => {
|
|
2974
3127
|
if (req.method === "GET") {
|
|
2975
|
-
writeJson(res, 200,
|
|
2976
|
-
ok: true,
|
|
2977
|
-
pluginVersion: CURRENT_VERSION,
|
|
2978
|
-
config: configOf(deps),
|
|
2979
|
-
saved: savedOf(deps)
|
|
2980
|
-
});
|
|
3128
|
+
writeJson(res, 200, configResponse(configOf(deps), savedOf(deps)));
|
|
2981
3129
|
return;
|
|
2982
3130
|
}
|
|
2983
3131
|
const raw = body;
|
|
@@ -3038,12 +3186,7 @@ function configRoutes(deps) {
|
|
|
3038
3186
|
else merged[key] = value;
|
|
3039
3187
|
config = merged;
|
|
3040
3188
|
} else config = await deps.updateConfig(patch);
|
|
3041
|
-
writeJson(res, 200,
|
|
3042
|
-
ok: true,
|
|
3043
|
-
pluginVersion: CURRENT_VERSION,
|
|
3044
|
-
config,
|
|
3045
|
-
saved: savedOf(deps)
|
|
3046
|
-
});
|
|
3189
|
+
writeJson(res, 200, configResponse(config, savedOf(deps)));
|
|
3047
3190
|
}
|
|
3048
3191
|
},
|
|
3049
3192
|
{
|
|
@@ -3133,11 +3276,65 @@ async function replaceSkillDir(targetDir, download) {
|
|
|
3133
3276
|
//#region src/routes/market.ts
|
|
3134
3277
|
/**
|
|
3135
3278
|
* 市场域路由:market 列表 / source 增删 / ref 锁定 / versions 选择器 /
|
|
3136
|
-
* check 更新检查 / stats 星标统计 / sync
|
|
3137
|
-
* handler 逻辑不变。
|
|
3279
|
+
* source/check 更新检查 / stats 星标统计 / source/sync 对齐版本。
|
|
3280
|
+
* 从 routes.ts 原样搬出,handler 逻辑不变。
|
|
3138
3281
|
*/
|
|
3139
3282
|
/** 市场域全部路由 spec(由 routes.ts 经 createRoute 包上统一围栏)。 */
|
|
3140
3283
|
function marketRoutes(deps) {
|
|
3284
|
+
const checkMarketSources = async ({ res }) => {
|
|
3285
|
+
const sources = await deps.store.listMarketSources();
|
|
3286
|
+
if (lastMarketCheck.size > 500) lastMarketCheck.clear();
|
|
3287
|
+
const results = [];
|
|
3288
|
+
for (const source of sources) {
|
|
3289
|
+
const base = {
|
|
3290
|
+
repo: source.repo,
|
|
3291
|
+
...source.ref !== void 0 ? { ref: source.ref } : {}
|
|
3292
|
+
};
|
|
3293
|
+
const now = Date.now();
|
|
3294
|
+
if (now - (lastMarketCheck.get(source.repo) ?? 0) < 3e5) {
|
|
3295
|
+
results.push({
|
|
3296
|
+
...base,
|
|
3297
|
+
updateAvailable: false,
|
|
3298
|
+
commitSha: source.commitSha ?? "",
|
|
3299
|
+
throttled: true
|
|
3300
|
+
});
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
try {
|
|
3304
|
+
const latestTag = await getLatestReleaseTag(source.repo);
|
|
3305
|
+
if (source.ref === void 0) {
|
|
3306
|
+
results.push({
|
|
3307
|
+
...base,
|
|
3308
|
+
updateAvailable: false,
|
|
3309
|
+
commitSha: source.commitSha ?? "",
|
|
3310
|
+
...latestTag !== void 0 ? { latestTag } : {}
|
|
3311
|
+
});
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
const latest = await getLatestCommit(source.repo, source.ref);
|
|
3315
|
+
lastMarketCheck.set(source.repo, now);
|
|
3316
|
+
const commitMoved = source.commitSha !== void 0 && latest.commitSha !== source.commitSha;
|
|
3317
|
+
const newRelease = latestTag !== void 0 && latestTag !== source.ref;
|
|
3318
|
+
results.push({
|
|
3319
|
+
...base,
|
|
3320
|
+
updateAvailable: commitMoved || newRelease,
|
|
3321
|
+
commitSha: latest.commitSha,
|
|
3322
|
+
...newRelease ? { latestTag } : {}
|
|
3323
|
+
});
|
|
3324
|
+
} catch (error) {
|
|
3325
|
+
results.push({
|
|
3326
|
+
...base,
|
|
3327
|
+
updateAvailable: false,
|
|
3328
|
+
commitSha: source.commitSha ?? "",
|
|
3329
|
+
error: errorText(error)
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
writeJson(res, 200, {
|
|
3334
|
+
ok: true,
|
|
3335
|
+
results
|
|
3336
|
+
});
|
|
3337
|
+
};
|
|
3141
3338
|
return [
|
|
3142
3339
|
{
|
|
3143
3340
|
path: SKILL_HUB_API.market,
|
|
@@ -3235,60 +3432,12 @@ function marketRoutes(deps) {
|
|
|
3235
3432
|
{
|
|
3236
3433
|
path: SKILL_HUB_API.marketCheck,
|
|
3237
3434
|
methods: ["GET"],
|
|
3238
|
-
handler:
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
repo: source.repo,
|
|
3245
|
-
...source.ref !== void 0 ? { ref: source.ref } : {}
|
|
3246
|
-
};
|
|
3247
|
-
const now = Date.now();
|
|
3248
|
-
if (now - (lastMarketCheck.get(source.repo) ?? 0) < 3e5) {
|
|
3249
|
-
results.push({
|
|
3250
|
-
...base,
|
|
3251
|
-
updateAvailable: false,
|
|
3252
|
-
commitSha: source.commitSha ?? "",
|
|
3253
|
-
throttled: true
|
|
3254
|
-
});
|
|
3255
|
-
continue;
|
|
3256
|
-
}
|
|
3257
|
-
try {
|
|
3258
|
-
const latestTag = await getLatestReleaseTag(source.repo);
|
|
3259
|
-
if (source.ref === void 0) {
|
|
3260
|
-
results.push({
|
|
3261
|
-
...base,
|
|
3262
|
-
updateAvailable: false,
|
|
3263
|
-
commitSha: source.commitSha ?? "",
|
|
3264
|
-
...latestTag !== void 0 ? { latestTag } : {}
|
|
3265
|
-
});
|
|
3266
|
-
continue;
|
|
3267
|
-
}
|
|
3268
|
-
const latest = await getLatestCommit(source.repo, source.ref);
|
|
3269
|
-
lastMarketCheck.set(source.repo, now);
|
|
3270
|
-
const commitMoved = source.commitSha !== void 0 && latest.commitSha !== source.commitSha;
|
|
3271
|
-
const newRelease = latestTag !== void 0 && latestTag !== source.ref;
|
|
3272
|
-
results.push({
|
|
3273
|
-
...base,
|
|
3274
|
-
updateAvailable: commitMoved || newRelease,
|
|
3275
|
-
commitSha: latest.commitSha,
|
|
3276
|
-
...newRelease ? { latestTag } : {}
|
|
3277
|
-
});
|
|
3278
|
-
} catch (error) {
|
|
3279
|
-
results.push({
|
|
3280
|
-
...base,
|
|
3281
|
-
updateAvailable: false,
|
|
3282
|
-
commitSha: source.commitSha ?? "",
|
|
3283
|
-
error: errorText(error)
|
|
3284
|
-
});
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
writeJson(res, 200, {
|
|
3288
|
-
ok: true,
|
|
3289
|
-
results
|
|
3290
|
-
});
|
|
3291
|
-
}
|
|
3435
|
+
handler: checkMarketSources
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
path: SKILL_HUB_API_DEPRECATED_MARKET_CHECK,
|
|
3439
|
+
methods: ["GET"],
|
|
3440
|
+
handler: checkMarketSources
|
|
3292
3441
|
},
|
|
3293
3442
|
{
|
|
3294
3443
|
path: SKILL_HUB_API.marketStats,
|
|
@@ -3511,7 +3660,7 @@ function repoImportRoutes(deps) {
|
|
|
3511
3660
|
for (const entry of selected) {
|
|
3512
3661
|
if (controller.signal.aborted) break;
|
|
3513
3662
|
job.current = entry.name;
|
|
3514
|
-
job.currentFile = entry.
|
|
3663
|
+
job.currentFile = entry.path;
|
|
3515
3664
|
if (entry.existing) {
|
|
3516
3665
|
job.skipped.push({
|
|
3517
3666
|
name: entry.name,
|
|
@@ -3535,7 +3684,7 @@ function repoImportRoutes(deps) {
|
|
|
3535
3684
|
try {
|
|
3536
3685
|
const result = await downloadRepoSkill(repo, resolvedRef, entry, files, targetRoot, fetch, controller.signal, (bytes, file) => {
|
|
3537
3686
|
job.downloadedBytes += bytes;
|
|
3538
|
-
job.currentFile = entry.dir
|
|
3687
|
+
job.currentFile = skillPathIn(entry.dir, file);
|
|
3539
3688
|
});
|
|
3540
3689
|
await deps.store.addSourceSkill(repo, entry.root, commitSha, resolvedRef, entry.name);
|
|
3541
3690
|
await deps.store.mergeSourceManifest(repo, skillManifest(tree, entry.dir), entry.dir);
|
|
@@ -3913,7 +4062,7 @@ function sourceRoutes(deps) {
|
|
|
3913
4062
|
try {
|
|
3914
4063
|
const entry = repoSkillEntry(name, source.root, repo);
|
|
3915
4064
|
entry.dir = skillDirOf(source, name, tree.map((item) => item.path));
|
|
3916
|
-
entry.path = entry.dir
|
|
4065
|
+
entry.path = skillFileAt(entry.dir);
|
|
3917
4066
|
const files = collectRepoSkillFiles(tree, entry.dir);
|
|
3918
4067
|
if (files.length === 0) {
|
|
3919
4068
|
failed.push({
|
|
@@ -4100,17 +4249,25 @@ function sourceRoutes(deps) {
|
|
|
4100
4249
|
/**
|
|
4101
4250
|
* Build every /api/skill-hub route.
|
|
4102
4251
|
* @param deps - skill registry view + sidecar store.
|
|
4103
|
-
* @returns the exact-path routes.
|
|
4252
|
+
* @returns the exact-path routes plus the family's 404 catch-all.
|
|
4104
4253
|
*/
|
|
4105
4254
|
function makeRoutes(deps) {
|
|
4106
|
-
return [
|
|
4255
|
+
return [...[
|
|
4107
4256
|
...catalogRoutes(deps),
|
|
4108
4257
|
...configRoutes(deps),
|
|
4109
4258
|
...marketRoutes(deps),
|
|
4110
4259
|
...repoImportRoutes(deps),
|
|
4111
4260
|
...groupRoutes(deps),
|
|
4112
4261
|
...sourceRoutes(deps)
|
|
4113
|
-
].map((spec) => createRoute(deps, spec))
|
|
4262
|
+
].map((spec) => createRoute(deps, spec)), createRoute(deps, {
|
|
4263
|
+
kind: "prefix",
|
|
4264
|
+
path: SKILL_HUB_API_ROOT,
|
|
4265
|
+
methods: ["GET", "POST"],
|
|
4266
|
+
skipGate: true,
|
|
4267
|
+
handler: async ({ res, url }) => {
|
|
4268
|
+
writeError(res, 404, "unknown skill-hub route: " + url.pathname + " (see SKILL_HUB_API in src/protocol/api.ts)");
|
|
4269
|
+
}
|
|
4270
|
+
})];
|
|
4114
4271
|
}
|
|
4115
4272
|
//#endregion
|
|
4116
4273
|
//#region src/stats/scan.ts
|
|
@@ -4493,56 +4650,100 @@ async function reconcileDisabledSkills(store, home = dshHome()) {
|
|
|
4493
4650
|
//#region src/index.ts
|
|
4494
4651
|
/** Stable cordis plugin name (matches cordis.patch.yml insert id). */
|
|
4495
4652
|
const name = "skill-hub";
|
|
4496
|
-
/** Services required before the skill-hub surfaces can mount. */
|
|
4653
|
+
/** Services required before the skill-hub surfaces can mount. `settings` is deliberately NOT here: the plugin reads its own volatile config refs and only reaches for the settings service when it is present (issue #11). */
|
|
4497
4654
|
const inject = [
|
|
4498
4655
|
"webServer",
|
|
4499
4656
|
"skills",
|
|
4500
|
-
"systemPrompt"
|
|
4501
|
-
"settings"
|
|
4657
|
+
"systemPrompt"
|
|
4502
4658
|
];
|
|
4659
|
+
/**
|
|
4660
|
+
* The durable field schemas, plain (non-volatile) so the same definitions can
|
|
4661
|
+
* back both the live view below and the wire form the settings page renders
|
|
4662
|
+
* from `.toJSON()`. `description` is what the auto-generated page shows with
|
|
4663
|
+
* each row; `role('secret')` makes the settings layer redact the value on
|
|
4664
|
+
* every wire read.
|
|
4665
|
+
*/
|
|
4666
|
+
const ConfigFields = {
|
|
4667
|
+
enabled: z.boolean().default(HUB_CONFIG_DEFAULTS.enabled).description("关闭后技能中枢的路由、入口与公告全部下线。"),
|
|
4668
|
+
announceToAgent: z.boolean().default(HUB_CONFIG_DEFAULTS.announceToAgent).description("在系统提示中加入本插件说明,用户提到技能管理时 Agent 知道如何协作。"),
|
|
4669
|
+
dotModelColor: z.string().pattern(HEX_COLOR_RE).description("技能行与聊天「/」菜单中「模型可调」圆点的颜色(#rrggbb)。"),
|
|
4670
|
+
dotUserColor: z.string().pattern(HEX_COLOR_RE).description("技能行与聊天「/」菜单中「仅用户可调」圆点的颜色(#rrggbb)。"),
|
|
4671
|
+
showUseCount: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseCount).description("在技能名旁显示调用次数。"),
|
|
4672
|
+
showUseTime: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseTime).description("在技能名行显示最近调用时间。"),
|
|
4673
|
+
showGroupSummary: z.boolean().default(HUB_CONFIG_DEFAULTS.showGroupSummary).description("在分组标题后汇总调用次数与最近调用时间。"),
|
|
4674
|
+
statsWindowDays: z.number().min(0).max(3650).default(HUB_CONFIG_DEFAULTS.statsWindowDays).description("只统计最近 N 天的使用次数;0 = 全部历史。"),
|
|
4675
|
+
statsScanMinutes: z.number().min(1).max(1440).default(HUB_CONFIG_DEFAULTS.statsScanMinutes).description("后台扫描会话日志的间隔(分钟,最小 1)。"),
|
|
4676
|
+
githubToken: z.string().role("secret").description("市场/来源走 GitHub API:匿名每小时 60 次,填 token 后 5000 次。留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。")
|
|
4677
|
+
};
|
|
4678
|
+
/**
|
|
4679
|
+
* The live plugin config the settings page edits. Field order here is the row
|
|
4680
|
+
* order the auto-generated page renders.
|
|
4681
|
+
*/
|
|
4503
4682
|
const Config = z.object({
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4683
|
+
enabled: ConfigFields.enabled.volatile(),
|
|
4684
|
+
announceToAgent: ConfigFields.announceToAgent.volatile(),
|
|
4685
|
+
dotModelColor: ConfigFields.dotModelColor.volatile(),
|
|
4686
|
+
dotUserColor: ConfigFields.dotUserColor.volatile(),
|
|
4687
|
+
showUseCount: ConfigFields.showUseCount.volatile(),
|
|
4688
|
+
showUseTime: ConfigFields.showUseTime.volatile(),
|
|
4689
|
+
showGroupSummary: ConfigFields.showGroupSummary.volatile(),
|
|
4690
|
+
statsWindowDays: ConfigFields.statsWindowDays.volatile(),
|
|
4691
|
+
statsScanMinutes: ConfigFields.statsScanMinutes.volatile(),
|
|
4692
|
+
githubToken: ConfigFields.githubToken.volatile()
|
|
4511
4693
|
});
|
|
4512
4694
|
/**
|
|
4513
|
-
*
|
|
4514
|
-
*
|
|
4515
|
-
*
|
|
4516
|
-
* page edit this namespace through the official settings transport, and the
|
|
4517
|
-
* plugin consumes the same resolved value — one source of truth.
|
|
4695
|
+
* The settings namespace this plugin's config lives under, narrowed to the
|
|
4696
|
+
* settings package's branded type. The browser half resolves the same form
|
|
4697
|
+
* through `ctx.configForms.get(HUB_ENTRY_ID)`.
|
|
4518
4698
|
*/
|
|
4519
|
-
const
|
|
4520
|
-
/**
|
|
4521
|
-
const
|
|
4522
|
-
enabled
|
|
4523
|
-
announceToAgent
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4699
|
+
const ENTRY_ID = HUB_ENTRY_ID;
|
|
4700
|
+
/** Every config field, in the order the settings page renders them (used to walk the volatile refs). */
|
|
4701
|
+
const CONFIG_FIELDS = [
|
|
4702
|
+
"enabled",
|
|
4703
|
+
"announceToAgent",
|
|
4704
|
+
"dotModelColor",
|
|
4705
|
+
"dotUserColor",
|
|
4706
|
+
"showUseCount",
|
|
4707
|
+
"showUseTime",
|
|
4708
|
+
"showGroupSummary",
|
|
4709
|
+
"statsWindowDays",
|
|
4710
|
+
"statsScanMinutes",
|
|
4711
|
+
"githubToken"
|
|
4712
|
+
];
|
|
4533
4713
|
/** Order of the announcement section within the tool-guidance band. */
|
|
4534
4714
|
const SECTION_ORDER = 152;
|
|
4535
4715
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
4536
|
-
const SKILL_HUB_GUIDANCE = ["本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置
|
|
4716
|
+
const SKILL_HUB_GUIDANCE = ["本机已安装 dsh-skill-hub 插件(DSH Web GUI 技能中枢):设置 →「技能」分区为管理主页;本插件的配置页(启用/公告开关、圆点颜色、GitHub token、统计窗口)在插件管理页——侧边栏「插件」→ 本插件,是本插件自己注册的配置卡片(默认折叠,点标题展开)。能力:完整本地技能目录(项目/自定义/用户/内置全部来源,走官方 ctx.skills 注册表,含第三方 provider);按来源与自定义分组浏览,分组/来源头部的滑动开关可一键启用/禁用整组(跨组冲突时询问);市场:内置市场目录(精选仓库一键添加)加自定义仓库源,扫描后勾选安装,每个市场源行显示已装/可更新/上游已删数量,支持「检查全部」与「全部更新」;来源跟踪:从 GitHub 仓库(市场源或直接地址)导入的技能记录上游 repo/commit 快照,可检查更新、选择同步、上游删除时跟进删除(移入回收站可恢复,恢复后保留来源与场景归属);个人技能(无来源记录)不跟踪;调用次数与最近使用时间统计;查看技能正文;发现诊断;新建技能向导(写入 ~/.dsh/skills 或 ~/.agents/skills)。限制:仅用户级技能(user-dsh/user-agents 根目录)可写,项目/内置/运行时技能只读展示;路由仅回环可访问。用户提到「技能管理 / 技能列表 / 技能开关 / 技能同步 / 技能市场 / 更新技能 / 新建技能」时即指本插件,请据此协作。", "The dsh-skill-hub plugin is installed (the DSH Web GUI skill hub): Settings → \"Skills\" is the management page; the plugin's configuration page (enable / announcement toggles, dot colors, GitHub token, stats window) is registered by the plugin itself on its own page in the Plugins manager (sidebar → 插件 → the plugin; collapsed until you expand the title). Capabilities: full local skill catalog (project / custom / user / bundled roots via the official ctx.skills registry, including third-party providers); browsing by source and custom groups, each group header carrying a sliding switch to enable/disable the whole group in one click (cross-group conflicts prompt the user); market: a built-in catalog of curated repos (one-click add) plus custom repo sources, scan-and-install import, per-source installed / updatable / deleted-upstream badges with \"check all\" and \"update all\" actions; upstream source tracking: skills imported from GitHub repos (market sources or direct URLs) record the repo/commit snapshot, support update checks, selective sync, and follow-up deletion when the upstream removes a skill (moves it into a restorable trash; restoring keeps the source and scene membership); personal skills (no source record) are never tracked; invocation counts and last-used times; skill body inspection; discovery diagnostics; new-skill wizard (writes to ~/.dsh/skills or ~/.agents/skills). Limits: only user-level skills (user-dsh/user-agents roots) are writable; project/bundled/runtime skills are read-only; routes are loopback-only. When the user mentions \"skill management / skill list / skill toggle / skill sync / skill market / update skills / new skill\", this plugin is what they mean — collaborate accordingly."].join("\n\n");
|
|
4537
4717
|
/**
|
|
4538
4718
|
* Mount the skill hub routes and announcement.
|
|
4539
4719
|
* @param ctx - host plugin context carrying webServer/skills/systemPrompt/settings.
|
|
4540
|
-
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
4541
4720
|
*/
|
|
4542
4721
|
function apply(ctx, config) {
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4722
|
+
/**
|
|
4723
|
+
* Read the runtime config out of our own volatile config refs.
|
|
4724
|
+
*
|
|
4725
|
+
* This is the whole point of `volatile()`: the Loader updates those values
|
|
4726
|
+
* **in place** (`ConfigEditor.edit` → `resolveConfig(fiber.runtime, next)` →
|
|
4727
|
+
* `updateVolatile`) without rebuilding the fiber, so a held reference always
|
|
4728
|
+
* answers `.get()` with the current value. Consequences worth remembering:
|
|
4729
|
+
* - no `settings.describe()` per request (it walks and projects every entry
|
|
4730
|
+
* in the profile — the reason this used to be slow);
|
|
4731
|
+
* - no subscription or watcher is needed for *reading*;
|
|
4732
|
+
* - the plugin keeps working when the Settings service is absent
|
|
4733
|
+
* (issue #11: business logic must not depend on Settings).
|
|
4734
|
+
*/
|
|
4735
|
+
const readConfig = () => {
|
|
4736
|
+
const out = {};
|
|
4737
|
+
for (const field of CONFIG_FIELDS) {
|
|
4738
|
+
const ref = config?.[field];
|
|
4739
|
+
const value = ref !== void 0 && typeof ref.get === "function" ? ref.get() : ref;
|
|
4740
|
+
if (value !== void 0) out[field] = value;
|
|
4741
|
+
}
|
|
4742
|
+
return out;
|
|
4743
|
+
};
|
|
4744
|
+
const current = () => resolveHubConfig({}, readConfig());
|
|
4745
|
+
/** The Settings service, when this deployment mounts it. Absent ⇒ no user layer to read or write. */
|
|
4746
|
+
const settingsOf = () => ctx.get("settings");
|
|
4546
4747
|
const store = new SkillHubStore();
|
|
4547
4748
|
let disposeRoutes;
|
|
4548
4749
|
let disposeSection;
|
|
@@ -4550,14 +4751,26 @@ function apply(ctx, config) {
|
|
|
4550
4751
|
let providerControl;
|
|
4551
4752
|
let stats;
|
|
4552
4753
|
const saved = () => {
|
|
4553
|
-
return
|
|
4754
|
+
return (settingsOf()?.describe().find((entry) => entry.ns === ENTRY_ID))?.user ?? {};
|
|
4554
4755
|
};
|
|
4555
4756
|
const updateConfig = async (patch) => {
|
|
4556
|
-
const
|
|
4557
|
-
for (const [
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4757
|
+
const ops = [];
|
|
4758
|
+
for (const [field, value] of Object.entries(patch)) if (value === void 0) ops.push({
|
|
4759
|
+
op: "unset",
|
|
4760
|
+
path: [field]
|
|
4761
|
+
});
|
|
4762
|
+
else ops.push({
|
|
4763
|
+
op: "set",
|
|
4764
|
+
path: [field],
|
|
4765
|
+
value
|
|
4766
|
+
});
|
|
4767
|
+
const settings = settingsOf();
|
|
4768
|
+
if (settings === void 0) throw new Error("this deployment does not mount the settings service; config is read-only");
|
|
4769
|
+
if (ops.length > 0) {
|
|
4770
|
+
await settings.mutate(ENTRY_ID, ops);
|
|
4771
|
+
sync();
|
|
4772
|
+
}
|
|
4773
|
+
return current();
|
|
4561
4774
|
};
|
|
4562
4775
|
const sync = () => {
|
|
4563
4776
|
if (disposeSection !== void 0) {
|
|
@@ -4582,6 +4795,7 @@ function apply(ctx, config) {
|
|
|
4582
4795
|
return new SkillHubProvider(control);
|
|
4583
4796
|
}), "dsh-skill-hub: provider");
|
|
4584
4797
|
}
|
|
4798
|
+
ctx.logger.info(`[dsh-skill-hub] surfaces synced: enabled=${value.enabled} announceToAgent=${value.announceToAgent}`);
|
|
4585
4799
|
if (disposeRoutes !== void 0) {
|
|
4586
4800
|
disposeRoutes();
|
|
4587
4801
|
disposeRoutes = void 0;
|
|
@@ -4604,9 +4818,6 @@ function apply(ctx, config) {
|
|
|
4604
4818
|
}, "dsh-skill-hub: routes");
|
|
4605
4819
|
};
|
|
4606
4820
|
sync();
|
|
4607
|
-
ctx.effect(() => settingsScope.watch(() => {
|
|
4608
|
-
sync();
|
|
4609
|
-
}), "dsh-skill-hub: settings config watch");
|
|
4610
4821
|
(async () => {
|
|
4611
4822
|
const home = dshHome();
|
|
4612
4823
|
const agentsHome = process.env.DSH_AGENTS_HOME ?? join(homedir(), ".agents");
|
|
@@ -4626,7 +4837,10 @@ function apply(ctx, config) {
|
|
|
4626
4837
|
(async () => {
|
|
4627
4838
|
try {
|
|
4628
4839
|
const legacy = await store.getConfig();
|
|
4629
|
-
if (Object.keys(legacy).length > 0 && Object.keys(saved()).length === 0)
|
|
4840
|
+
if (Object.keys(legacy).length > 0 && Object.keys(saved()).length === 0) {
|
|
4841
|
+
const settings = settingsOf();
|
|
4842
|
+
if (settings !== void 0) await settings.update(ENTRY_ID, legacy);
|
|
4843
|
+
}
|
|
4630
4844
|
} catch (error) {
|
|
4631
4845
|
ctx.logger.warn("[dsh-skill-hub] sidecar config migration into the settings namespace failed", error);
|
|
4632
4846
|
}
|
|
@@ -4706,4 +4920,4 @@ function apply(ctx, config) {
|
|
|
4706
4920
|
});
|
|
4707
4921
|
}
|
|
4708
4922
|
//#endregion
|
|
4709
|
-
export {
|
|
4923
|
+
export { Config, ENTRY_ID, SKILL_HUB_GUIDANCE, apply, inject, name };
|