dsh-skill-hub 0.3.19 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +1 -1
- package/README.md +4 -3
- package/README.zh.md +4 -3
- package/lib/client.js +10 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +428 -156
- package/lib/types/client/api.d.ts +3 -3
- package/lib/types/protocol/config.d.ts +17 -11
- package/lib/types/protocol/market.d.ts +11 -0
- package/lib/types/protocol/sources.d.ts +5 -0
- package/lib/types/protocol.d.ts +1 -1
- package/lib/types/reconcile.d.ts +24 -0
- package/lib/types/repo/github-client.d.ts +11 -4
- package/lib/types/repo.d.ts +1 -1
- package/lib/types/routes/deps.d.ts +6 -1
- package/lib/types/routes/route-state.d.ts +22 -2
- package/lib/types/skillfs/scan.d.ts +32 -5
- package/lib/types/skillfs.d.ts +34 -2
- package/lib/types/store/migrate.d.ts +1 -1
- package/lib/types/store/store.d.ts +10 -5
- package/package.json +21 -20
- package/src/client/api.ts +4 -3
- package/src/client/panel/hooks/useMarketFlow.ts +4 -2
- package/src/client/settings-card.tsx +2 -1
- package/src/client/settings-form.ts +6 -3
- package/src/index.ts +12 -2
- package/src/protocol/config.ts +27 -24
- package/src/protocol/market.ts +12 -0
- package/src/protocol/sources.ts +5 -0
- package/src/protocol.ts +1 -0
- package/src/reconcile.test.ts +74 -1
- package/src/reconcile.ts +50 -1
- package/src/repo/github-client.ts +26 -14
- package/src/repo.test.ts +20 -1
- package/src/repo.ts +1 -1
- package/src/routes/catalog-data.ts +7 -4
- package/src/routes/catalog.ts +51 -28
- package/src/routes/deps.ts +7 -2
- package/src/routes/market.ts +13 -2
- package/src/routes/repo-import.ts +17 -30
- package/src/routes/route-state.test.ts +4 -4
- package/src/routes/route-state.ts +45 -10
- package/src/routes/sources.ts +79 -23
- package/src/routes.test.ts +300 -0
- package/src/skillfs/scan.ts +119 -17
- package/src/skillfs.test.ts +64 -3
- package/src/skillfs.ts +51 -8
- package/src/store/migrate.ts +6 -4
- package/src/store/store.ts +21 -11
- package/src/store.test.ts +17 -1
- package/src/update.ts +6 -6
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Browser-side API client for the /api/skill-hub route family. The only
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
|
-
import { type CatalogResponse, type ConfigResponse, type CreateRequest, type CreateResponse, type DiagnosticFixResponse, type GroupsResponse, type MarketCheckResponse, type MarketSourcesResponse, type MarketSourceVersionsResponse, type MarketStatsResponse, type MarketSyncResponse, type RepoDiscoverResponse, type RepoImportCancelResponse, type RepoImportProgressResponse, type RepoImportResponse, type SkillDetail, type SkillDeleteResponse, type SkillTag, type SourceCheckResponse, type SourceDeleteResponse, type SourceRestoreResponse, type SourceTrashClearResponse, type SourcesResponse, type SourceSyncResponse, type StatsResponse, type TagSaveRequest, type ToggleBatchResponse, type UpdateCheckResponse } from '../protocol.ts';
|
|
5
|
+
import { type CatalogResponse, type ConfigResponse, type CreateRequest, type CreateResponse, type DiagnosticFixResponse, type GroupsResponse, type MarketCheckResponse, type MarketSourceAddResponse, type MarketSourcesResponse, type MarketSourceVersionsResponse, type MarketStatsResponse, type MarketSyncResponse, type RepoDiscoverResponse, type RepoImportCancelResponse, type RepoImportProgressResponse, type RepoImportResponse, type SkillDetail, type SkillDeleteResponse, type SkillTag, type SourceCheckResponse, type SourceDeleteResponse, type SourceRestoreResponse, type SourceTrashClearResponse, type SourcesResponse, type SourceSyncResponse, type StatsResponse, type TagSaveRequest, type ToggleBatchResponse, type UpdateCheckResponse } from '../protocol.ts';
|
|
6
6
|
/** Error carrying the route's JSON error message. */
|
|
7
7
|
export declare class SkillHubApiError extends Error {
|
|
8
8
|
constructor(message: string);
|
|
@@ -31,8 +31,8 @@ export declare class SkillHubApi {
|
|
|
31
31
|
toggleBatch(names: string[], enabled: boolean): Promise<ToggleBatchResponse>;
|
|
32
32
|
/** The user's added market sources. */
|
|
33
33
|
market(): Promise<MarketSourcesResponse>;
|
|
34
|
-
/** Add a market source repo; resolves with the fresh list. */
|
|
35
|
-
addMarketSource(repo: string): Promise<
|
|
34
|
+
/** Add a market source repo; resolves with the fresh list plus the resolved slug. */
|
|
35
|
+
addMarketSource(repo: string): Promise<MarketSourceAddResponse>;
|
|
36
36
|
/** Remove a market source repo; resolves with the fresh list. */
|
|
37
37
|
removeMarketSource(repo: string): Promise<MarketSourcesResponse>;
|
|
38
38
|
/** Stars + downloads per market source (hourly server-side cache; refresh=1 refetches stale entries). */
|
|
@@ -83,13 +83,17 @@ export declare const HUB_CONFIG_DEFAULTS: {
|
|
|
83
83
|
readonly statsScanMinutes: 5;
|
|
84
84
|
};
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
86
|
+
* Fill a raw settings value into the effective hub config: schema defaults for
|
|
87
|
+
* anything missing, and the numeric stats knobs clamped to sane ranges
|
|
88
|
+
* (window ≥ 0, scan interval ≥ 1 minute).
|
|
89
|
+
*
|
|
90
|
+
* There is deliberately no "saved sidecar layer" parameter. Until dsh 0.1.7 the
|
|
91
|
+
* hub kept its own runtime overrides in the sidecar and merged them here; the
|
|
92
|
+
* settings namespace is now the only user layer (`updateConfig` writes through
|
|
93
|
+
* `ctx.settings.mutate`), so a merge against a sidecar layer could never
|
|
94
|
+
* observe anything but `{}` — see the git history of `resolveHubConfig`.
|
|
91
95
|
*/
|
|
92
|
-
export declare function resolveHubConfig(
|
|
96
|
+
export declare function resolveHubConfig(base?: Partial<HubConfig>): HubConfig;
|
|
93
97
|
/**
|
|
94
98
|
* Drop the GitHub token from a config-shaped object. Returns a copy, so the
|
|
95
99
|
* caller's own config layer keeps the token. Shared by the config route's GET
|
|
@@ -112,7 +116,13 @@ export interface ConfigResponse {
|
|
|
112
116
|
/** True when a GitHub token is in effect (saved override or the base/env layer). */
|
|
113
117
|
githubTokenSet: boolean;
|
|
114
118
|
}
|
|
115
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* POST /api/skill-hub/config — a partial patch; omitted fields keep their values.
|
|
121
|
+
*
|
|
122
|
+
* Only the fields the handler actually parses belong here: the stats knobs are
|
|
123
|
+
* owned by the settings namespace (`statsWindowDays` / `statsScanMinutes` are
|
|
124
|
+
* still part of {@link HubSettingsValue}), not by this route.
|
|
125
|
+
*/
|
|
116
126
|
export interface ConfigRequest {
|
|
117
127
|
/** Set the field; null clears the saved override so it re-inherits the default. */
|
|
118
128
|
enabled?: boolean | null;
|
|
@@ -128,10 +138,6 @@ export interface ConfigRequest {
|
|
|
128
138
|
dotModelColor?: string | null;
|
|
129
139
|
/** Set the dot color; null clears the saved override so it re-inherits the default. */
|
|
130
140
|
dotUserColor?: string | null;
|
|
131
|
-
/** 统计滚动窗口天数(0 = 全部历史);null 清除覆盖回默认。 */
|
|
132
|
-
statsWindowDays?: number | null;
|
|
133
|
-
/** 自动统计扫描间隔(分钟);null 清除覆盖回默认。 */
|
|
134
|
-
statsScanMinutes?: number | null;
|
|
135
141
|
/** Set the GitHub token; null/empty clears it back to anonymous. */
|
|
136
142
|
githubToken?: string | null;
|
|
137
143
|
}
|
|
@@ -23,6 +23,17 @@ export interface MarketSourceResponse {
|
|
|
23
23
|
ok: true;
|
|
24
24
|
repos: MarketSourceRecord[];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* POST /api/skill-hub/market/source — same list, plus the slug the host
|
|
28
|
+
* resolved from the input. Clients must scan *this* slug rather than assume
|
|
29
|
+
* the acted-on source sits at some position in `repos`: re-adding a repo the
|
|
30
|
+
* user already tracks only updates its `ref` in place and never reorders the
|
|
31
|
+
* list.
|
|
32
|
+
*/
|
|
33
|
+
export interface MarketSourceAddResponse extends MarketSourceResponse {
|
|
34
|
+
/** Normalized `owner/repo` the host recorded for the submitted input. */
|
|
35
|
+
repo: string;
|
|
36
|
+
}
|
|
26
37
|
/** POST /api/skill-hub/market/source/ref — pin a market source to an explicit ref. */
|
|
27
38
|
export interface MarketSourceRefRequest {
|
|
28
39
|
repo: string;
|
|
@@ -134,6 +134,11 @@ export interface SourceTrashClearResponse {
|
|
|
134
134
|
name: string;
|
|
135
135
|
error: string;
|
|
136
136
|
}>;
|
|
137
|
+
/**
|
|
138
|
+
* `.trash` 里没有对应记录的残留目录(同名技能先后两次入站时,旧记录被覆盖,
|
|
139
|
+
* 目录却留了下来)。清空回收站时一并回收,这里上报它们的路径;没有则省略。
|
|
140
|
+
*/
|
|
141
|
+
orphans?: string[];
|
|
137
142
|
}
|
|
138
143
|
/** 项目级技能来源(它们有 workspace 归属,不属于「个人」组)。 */
|
|
139
144
|
export declare function isProjectSource(source: string): boolean;
|
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';
|
package/lib/types/reconcile.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface DisabledReconcileStore {
|
|
|
15
15
|
listDisabled(): Promise<DisabledSkill[]>;
|
|
16
16
|
addDisabled(entry: DisabledSkill): Promise<void>;
|
|
17
17
|
}
|
|
18
|
+
/** Narrow store view used by the reverse reconcile (adds record removal). */
|
|
19
|
+
export interface DisabledPruneStore extends DisabledReconcileStore {
|
|
20
|
+
removeDisabled(name: string): Promise<void>;
|
|
21
|
+
}
|
|
18
22
|
/**
|
|
19
23
|
* Add sidecar disabled records for every `.disabled` discovery file that has
|
|
20
24
|
* none yet. Existing records (matched by path or by name) win, so this never
|
|
@@ -22,3 +26,23 @@ export interface DisabledReconcileStore {
|
|
|
22
26
|
* in the diagnostics scan instead). Returns the records added.
|
|
23
27
|
*/
|
|
24
28
|
export declare function reconcileDisabledSkills(store: DisabledReconcileStore, home?: string): Promise<DisabledSkill[]>;
|
|
29
|
+
/** 反向对账的结果:修好路径的记录 + 被清掉的死记录。 */
|
|
30
|
+
export interface DisabledPruneResult {
|
|
31
|
+
/** 记录还在、但 path 指向的文件已不在:已改指向现存的 `.disabled` 文件。 */
|
|
32
|
+
repaired: DisabledSkill[];
|
|
33
|
+
/** 文件与禁用占位都已不存在:记录已删除,名字被释放。 */
|
|
34
|
+
removed: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 反向对账:`reconcileDisabledSkills` 只处理「磁盘有 `.disabled`、sidecar 没记录」。
|
|
38
|
+
* 反方向(sidecar 有记录、磁盘上那个文件已经不在了)以前永不清理,于是留下一条
|
|
39
|
+
* 干掉不掉的记录:`/toggle` 启用会 rename 失败,`/create` 被 409 挡住,`/skill/delete`
|
|
40
|
+
* 也 rename 失败 —— 名字被永久锁死(issue #16 类,实测 G5)。
|
|
41
|
+
*
|
|
42
|
+
* 两种处理,都不动启用态技能:
|
|
43
|
+
* - 名字在该 root 下仍有 `.disabled` 占位(目录包或扁平形态)→ 把记录指向真实文件;
|
|
44
|
+
* - 一个都没有 → 删记录,把名字还给用户。
|
|
45
|
+
*
|
|
46
|
+
* 只处理合法可写 root 的记录;root 值异常(历史损坏数据)时原样保留,不擅自删。
|
|
47
|
+
*/
|
|
48
|
+
export declare function pruneStaleDisabledSkills(store: DisabledPruneStore, home?: string): Promise<DisabledPruneResult>;
|
|
@@ -30,6 +30,14 @@ export declare function githubAuthHeaders(): Record<string, string>;
|
|
|
30
30
|
export declare const NO_COMPRESSION: Record<string, string>;
|
|
31
31
|
/** JSON API 默认请求头(含鉴权)。 */
|
|
32
32
|
export declare function apiHeaders(): Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* When the response shows an exhausted GitHub rate limit, the instant it
|
|
35
|
+
* resets; otherwise undefined. Shared by the repo fetch layer and the plugin's
|
|
36
|
+
* own update check so both agree on what counts as a rate-limit rejection
|
|
37
|
+
* (403/429 + `x-ratelimit-remaining: 0` + a parseable `x-ratelimit-reset`)
|
|
38
|
+
* instead of each re-deriving it from the headers.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rateLimitResetAt(response: Response): Date | undefined;
|
|
33
41
|
/**
|
|
34
42
|
* Build a RepoFetchError; when the response shows an exhausted rate limit,
|
|
35
43
|
* report the reset time instead of a bare HTTP status.
|
|
@@ -46,10 +54,9 @@ export declare function fetchJson(url: string, fetchImpl: typeof fetch, context:
|
|
|
46
54
|
response: Response;
|
|
47
55
|
}>;
|
|
48
56
|
/**
|
|
49
|
-
* Load
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the exact same content a later download would fetch.
|
|
57
|
+
* Load one JSON document from the GitHub API, reusing an ETag when the URL was
|
|
58
|
+
* fetched before. `fetchImpl`/`context` exist so callers can stub the transport
|
|
59
|
+
* and shape the error text; the payload is whatever the endpoint returned.
|
|
53
60
|
*/
|
|
54
61
|
export declare function fetchJsonCached(url: string, fetchImpl: typeof fetch, context: string): Promise<{
|
|
55
62
|
json: unknown;
|
package/lib/types/repo.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The implementation now lives under `./repo/` (types, discovery, api,
|
|
13
13
|
* install) and this module is a barrel that re-exports it.
|
|
14
14
|
*/
|
|
15
|
-
export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, setGithubToken } from './repo/github-client.ts';
|
|
15
|
+
export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, rateLimitResetAt, setGithubToken } from './repo/github-client.ts';
|
|
16
16
|
export type { RepoRef, RepoTreeItem, RepoFile } from './repo/types.ts';
|
|
17
17
|
export { REPO_ROOT, isRepoRoot, repoSlug, normalizeRepoInput, collectRepoSkillFiles, originForRoot, discoverRepoEntries, skillManifest, skillDirOf, skillFileAt, skillPathIn, relativeToSkillDir, diffRemoteSkills, repoSkillEntry, } from './repo/discovery.ts';
|
|
18
18
|
export { loadRepoTree, getLatestReleaseTag, getRepoStats, listRepoReleases, listRepoBranches, getLatestCommit, loadRepoTreeAt, } from './repo/api.ts';
|
|
@@ -37,7 +37,12 @@ export interface SkillHubRouteDeps {
|
|
|
37
37
|
}
|
|
38
38
|
/** The resolved hub config a route sees (the shared resolver fills defaults). */
|
|
39
39
|
export declare function configOf(deps: SkillHubRouteDeps): HubConfig;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The raw saved config layer as persisted in the sidecar, reported for display
|
|
42
|
+
* only. Since dsh 0.1.7 the settings namespace is the one user layer, so this
|
|
43
|
+
* is legacy data: nothing writes it any more and the effective config above
|
|
44
|
+
* never merges it.
|
|
45
|
+
*/
|
|
41
46
|
export declare function savedOf(deps: SkillHubRouteDeps): Partial<HubConfig>;
|
|
42
47
|
/** Refuse business routes while the master switch is off (the config route stays up). */
|
|
43
48
|
export declare function disabledGate(deps: SkillHubRouteDeps, res: ServerResponse): boolean;
|
|
@@ -54,5 +54,25 @@ export declare const importJobs: Map<string, ImportJob>;
|
|
|
54
54
|
export declare const IMPORT_JOB_TTL_MS: number;
|
|
55
55
|
export declare const IMPORT_JOB_MAX = 100;
|
|
56
56
|
export declare function gcImportJobs(): void;
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Replace whatever currently occupies a skill slot with a fresh download;
|
|
59
|
+
* restores the previous state on failure.
|
|
60
|
+
*
|
|
61
|
+
* `target` is the entry to move aside — a bundle **directory** or a flat
|
|
62
|
+
* `<name>.md` file, whichever the download is replacing (the rename/rm below
|
|
63
|
+
* are form-neutral). The backup is dot-prefixed (discovery skips dot entries),
|
|
64
|
+
* so even a failed cleanup can never register as a duplicate skill.
|
|
65
|
+
*/
|
|
66
|
+
export declare function replaceInPlace(target: string, download: () => Promise<unknown>): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* 让位:把一个名字下的「非启用态占位」移入回收站并清掉它的 sidecar 禁用记录。
|
|
69
|
+
* 只动这两类,**启用的技能一律不碰**:
|
|
70
|
+
* - 禁用发现文件(`<name>/SKILL.md.disabled` 或 `<name>.md.disabled`,两种形态都认);
|
|
71
|
+
* - 同名目录存在但既无 `SKILL.md` 也无 `SKILL.md.disabled`(导入中断/手工删除的空壳)。
|
|
72
|
+
*
|
|
73
|
+
* 回收站按名字索引,所以多形态只会记下最后一条的恢复指针;其余的那份会在
|
|
74
|
+
* 「清空回收站」时被孤儿回收扫掉(见 sourceTrashClear)。
|
|
75
|
+
*
|
|
76
|
+
* @returns 被移入回收站的路径列表,没有可让位的东西时 undefined。
|
|
77
|
+
*/
|
|
78
|
+
export declare function retireNamePlaceholders(deps: SkillHubRouteDeps, base: string, name: string): Promise<string[] | undefined>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DiagnosticEntry, type WritableRoot } from '../protocol.ts';
|
|
2
2
|
/** One discoverable skill file in a scanned root. */
|
|
3
3
|
export interface SkillEntry {
|
|
4
4
|
/** Absolute path of the discovery file (SKILL.md or the flat .md). */
|
|
@@ -39,9 +39,36 @@ export declare function readSkillInterface(directory: string): Promise<SkillInte
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function findProjectRoot(cwd: string): Promise<string>;
|
|
41
41
|
/**
|
|
42
|
-
* Scan one writable root for files the provider ignores, so the GUI can
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Scan one writable root for files the provider ignores, so the GUI can show
|
|
43
|
+
* why a skill never appears — a missing frontmatter, a name that disagrees
|
|
44
|
+
* with the discovery path, and the like must be visible, not silent. Files the
|
|
45
|
+
* hub itself disabled (`.disabled`) are skipped: they are a toggle state, not
|
|
46
|
+
* a defect.
|
|
46
47
|
*/
|
|
47
48
|
export declare function scanDiagnostics(root: WritableRoot, home?: string): Promise<DiagnosticEntry[]>;
|
|
49
|
+
/**
|
|
50
|
+
* 某个名字在一个 root 下现存的禁用发现文件(两种形态都认),没有则 undefined。
|
|
51
|
+
* 目录包形态优先(`<name>/SKILL.md.disabled`),其次扁平 `<name>.md.disabled`。
|
|
52
|
+
* 单点定义「禁用态在磁盘上长什么样」,避免各处再拼一次路径(issue #16 类)。
|
|
53
|
+
*/
|
|
54
|
+
export declare function disabledOccupancyIn(base: string, name: string): Promise<string | undefined>;
|
|
55
|
+
/**
|
|
56
|
+
* 一个名字在某个 root 下的全部磁盘占位。技能名是系统里唯一的身份,但同一个名字
|
|
57
|
+
* 可以被两种形态(目录包 `<name>/SKILL.md` / 扁平 `<name>.md`)× 两种状态
|
|
58
|
+
* (启用 / `.disabled` 改名)占据,而且两种形态可以**同时存在** —— 注册表只会
|
|
59
|
+
* 给出其中一个,另一个静默不可达。所有按名字操作文件的路径都应该从这里取值,
|
|
60
|
+
* 而不是各自拼一次路径(issue #16 类的根因)。
|
|
61
|
+
*
|
|
62
|
+
* 两个数组都按「目录包优先」排序:目录包是 hub 自己写入的形态(导入、创建、
|
|
63
|
+
* 同步都建目录包),冲突时它是更「权威」的那份。
|
|
64
|
+
*/
|
|
65
|
+
export interface NameOccupancy {
|
|
66
|
+
/** 启用态发现文件(目录包 `<name>/SKILL.md` 在前,扁平 `<name>.md` 在后)。 */
|
|
67
|
+
enabled: string[];
|
|
68
|
+
/** 禁用态发现文件(`<name>/SKILL.md.disabled` 在前,`<name>.md.disabled` 在后)。 */
|
|
69
|
+
disabled: string[];
|
|
70
|
+
/** `<name>/` 存在,但既没有 `SKILL.md` 也没有 `SKILL.md.disabled`(空壳/残留目录)。 */
|
|
71
|
+
bareDir?: string;
|
|
72
|
+
}
|
|
73
|
+
/** 读一个名字在某个 skills root 下的全部占位。只读,不修改磁盘。 */
|
|
74
|
+
export declare function occupancyOf(base: string, name: string): Promise<NameOccupancy>;
|
package/lib/types/skillfs.d.ts
CHANGED
|
@@ -29,11 +29,35 @@ export declare function createSkill(root: WritableRoot, name: string, descriptio
|
|
|
29
29
|
export declare function disableSkill(path: string): Promise<string>;
|
|
30
30
|
/** Re-enable a previously disabled file (reverse of disableSkill). */
|
|
31
31
|
export declare function enableSkill(disabledPath: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Move one already-resolved skill (a bundle directory or a flat .md file) into
|
|
34
|
+
* its root's trash as `<root>/.trash/<name>-<timestamp>`.
|
|
35
|
+
*
|
|
36
|
+
* The layout is the restore contract, not an implementation detail:
|
|
37
|
+
* `restoreSkill` renames the entry back and only proves the parent is `.trash`,
|
|
38
|
+
* and `clearTrash` deletes in place. Every producer of a trash entry must go
|
|
39
|
+
* through here — skill delete, the hub-disabled whole-group delete, and repo
|
|
40
|
+
* import's placeholder replacement all move skills for the same recovery flow.
|
|
41
|
+
*
|
|
42
|
+
* @returns the trashed path plus the original path that was moved.
|
|
43
|
+
*/
|
|
44
|
+
export declare function moveIntoTrash(source: string): Promise<{
|
|
45
|
+
path: string;
|
|
46
|
+
source: string;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* The filesystem entry behind a discovery file: a directory bundle is addressed
|
|
50
|
+
* by its `SKILL.md` (or `SKILL.md.disabled`) and moved as a whole directory, a
|
|
51
|
+
* flat `<name>.md` file is moved as itself. Single definition of that mapping —
|
|
52
|
+
* delete, restore, sync-replace and placeholder retirement must all agree on it,
|
|
53
|
+
* or one of them moves a bundle's discovery file and leaves an empty directory.
|
|
54
|
+
*/
|
|
55
|
+
export declare function trashSourceOf(discoveryPath: string): string;
|
|
32
56
|
/**
|
|
33
57
|
* Move a skill (directory bundle or flat .md file) into the root's trash.
|
|
34
58
|
* The skill stays recoverable: the source is renamed into
|
|
35
59
|
* <root>/.trash/<name>-<timestamp>. A directory bundle is addressed by its
|
|
36
|
-
* SKILL.md path and moved as a whole directory.
|
|
60
|
+
* SKILL.md (or SKILL.md.disabled) path and moved as a whole directory.
|
|
37
61
|
* @returns the trashed path plus the original path that was moved.
|
|
38
62
|
*/
|
|
39
63
|
export declare function trashSkill(sourcePath: string): Promise<{
|
|
@@ -44,5 +68,13 @@ export declare function trashSkill(sourcePath: string): Promise<{
|
|
|
44
68
|
export declare function restoreSkill(entry: TrashEntry, home?: string): Promise<string>;
|
|
45
69
|
/** Permanently delete one trashed skill (directory or flat file). */
|
|
46
70
|
export declare function clearTrash(entry: TrashEntry, home?: string): Promise<string>;
|
|
47
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Repair one file on disk when its frontmatter is auto-fixable. Returns the new text.
|
|
73
|
+
*
|
|
74
|
+
* The write is atomic (sibling temp file + rename), matching the sidecar's
|
|
75
|
+
* persist path: this rewrites a real skill file in place, and a crash midway
|
|
76
|
+
* through a plain write would leave a truncated SKILL.md — exactly the failure
|
|
77
|
+
* the store layer already guards against. The `.tmp` sibling is never picked
|
|
78
|
+
* up by discovery (only `SKILL.md` and `*.md` are scanned).
|
|
79
|
+
*/
|
|
48
80
|
export declare function fixDiagnosticFile(path: string, home?: string): Promise<string>;
|
|
@@ -31,7 +31,7 @@ export interface HydratedState {
|
|
|
31
31
|
tagsById: Map<string, SkillTag>;
|
|
32
32
|
sourcesByRepo: Map<string, SourceRecord>;
|
|
33
33
|
marketSources: MarketSourceRecord[];
|
|
34
|
-
|
|
34
|
+
trashByPath: Map<string, TrashEntry>;
|
|
35
35
|
skillStats: SkillStatsCheckpoint | undefined;
|
|
36
36
|
marketStats: MarketStatsSnapshot | undefined;
|
|
37
37
|
collectionOrder: string[];
|
|
@@ -7,7 +7,7 @@ export declare class SkillHubStore {
|
|
|
7
7
|
private tagsById;
|
|
8
8
|
private sourcesByRepo;
|
|
9
9
|
private marketSources;
|
|
10
|
-
private
|
|
10
|
+
private trashByPath;
|
|
11
11
|
private skillStats;
|
|
12
12
|
private marketStats;
|
|
13
13
|
private collectionOrder;
|
|
@@ -119,12 +119,17 @@ export declare class SkillHubStore {
|
|
|
119
119
|
setMarketSourceCommit(repo: string, commitSha: string): Promise<void>;
|
|
120
120
|
/** All trashed skills, newest first. */
|
|
121
121
|
listTrash(): Promise<TrashEntry[]>;
|
|
122
|
-
/** One trash entry by skill name
|
|
122
|
+
/** One trash entry by skill name — the newest when a name has several. */
|
|
123
123
|
getTrash(name: string): Promise<TrashEntry | undefined>;
|
|
124
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Record a trashed skill, keyed by its trashed path. Keying by name used to
|
|
126
|
+
* silently drop the previous record when the same name landed in the trash
|
|
127
|
+
* twice (a re-delete, or the two on-disk discovery forms), orphaning its
|
|
128
|
+
* directory with nothing left pointing at it.
|
|
129
|
+
*/
|
|
125
130
|
addTrash(entry: TrashEntry): Promise<void>;
|
|
126
|
-
/** Remove a trash record (after restore). */
|
|
127
|
-
removeTrash(
|
|
131
|
+
/** Remove a trash record by its trashed path (after restore or permanent clear). */
|
|
132
|
+
removeTrash(path: string): Promise<void>;
|
|
128
133
|
/** The persisted usage-statistics checkpoint (undefined until first saved). */
|
|
129
134
|
getSkillStatsState(): Promise<SkillStatsCheckpoint | undefined>;
|
|
130
135
|
/** Persist a usage-statistics checkpoint (after every completed scan; cadence follows the scan TTL). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "In-GUI skill hub for DeepSeek Harness (dsh): browse the full local skill catalog from the official ctx.skills registry (every root + third-party providers), toggle skills on/off, inspect bodies, surface frontmatter diagnostics, and scaffold new skills — plus a codex-style skill market (built-in catalog, upstream update checks, one-click update-all) with tracked source sync. The full manager beyond the read-only dsh-skill-manager browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"0.1.7-rc.1",
|
|
67
67
|
"0.1.7-rc.2",
|
|
68
68
|
"0.2.0-rc.1",
|
|
69
|
-
"0.2.0-rc.2"
|
|
69
|
+
"0.2.0-rc.2",
|
|
70
|
+
"0.2.1-alpha.2"
|
|
70
71
|
]
|
|
71
72
|
},
|
|
72
73
|
"capability": {
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
"js-yaml": "^4.1.0"
|
|
87
88
|
},
|
|
88
89
|
"peerDependencies": {
|
|
89
|
-
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2",
|
|
90
|
+
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2 || ^4.0.5-alpha.1",
|
|
90
91
|
"@deepseek-ai/dsh-client-connection": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
91
92
|
"@deepseek-ai/dsh-client-locale": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
92
93
|
"@deepseek-ai/dsh-client-runtime": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
@@ -100,7 +101,7 @@
|
|
|
100
101
|
"@deepseek-ai/dsh-settings": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
101
102
|
"@deepseek-ai/dsh-skill": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
102
103
|
"@deepseek-ai/dsh-system-prompt": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
103
|
-
"@deepseek-ai/schemastery": "^3.18.3",
|
|
104
|
+
"@deepseek-ai/schemastery": "^3.18.3 || ^3.18.5-alpha.1",
|
|
104
105
|
"react": "^18.2.0",
|
|
105
106
|
"react-dom": "^18.2.0"
|
|
106
107
|
},
|
|
@@ -113,22 +114,22 @@
|
|
|
113
114
|
}
|
|
114
115
|
},
|
|
115
116
|
"devDependencies": {
|
|
116
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
117
|
-
"@deepseek-ai/dsh-client-connection": "^0.2.
|
|
118
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
119
|
-
"@deepseek-ai/dsh-client-store": "^0.2.
|
|
120
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.2.
|
|
121
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.
|
|
122
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.
|
|
123
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.2.
|
|
124
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.
|
|
125
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.
|
|
126
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
127
|
-
"@deepseek-ai/dsh-session-query": "^0.2.
|
|
128
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
129
|
-
"@deepseek-ai/dsh-skill": "^0.2.
|
|
130
|
-
"@deepseek-ai/dsh-system-prompt": "^0.2.
|
|
131
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
117
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.1",
|
|
118
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-alpha.2",
|
|
119
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-alpha.2",
|
|
120
|
+
"@deepseek-ai/dsh-client-store": "^0.2.1-alpha.2",
|
|
121
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.2.1-alpha.2",
|
|
122
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-alpha.2",
|
|
123
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-alpha.2",
|
|
124
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.2.1-alpha.2",
|
|
125
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-alpha.2",
|
|
126
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.1-alpha.2",
|
|
127
|
+
"@deepseek-ai/dsh-session": "^0.2.1-alpha.2",
|
|
128
|
+
"@deepseek-ai/dsh-session-query": "^0.2.1-alpha.2",
|
|
129
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-alpha.2",
|
|
130
|
+
"@deepseek-ai/dsh-skill": "^0.2.1-alpha.2",
|
|
131
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.1-alpha.2",
|
|
132
|
+
"@deepseek-ai/schemastery": "^3.18.5-alpha.1",
|
|
132
133
|
"@types/js-yaml": "^4.0.9",
|
|
133
134
|
"@types/node": "^22.20.0",
|
|
134
135
|
"@types/react": "~18.3.1",
|
package/src/client/api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type DiagnosticFixResponse,
|
|
13
13
|
type GroupsResponse,
|
|
14
14
|
type MarketCheckResponse,
|
|
15
|
+
type MarketSourceAddResponse,
|
|
15
16
|
type MarketSourceRefRequest,
|
|
16
17
|
type MarketSourceRequest,
|
|
17
18
|
type MarketSourceResponse,
|
|
@@ -156,9 +157,9 @@ export class SkillHubApi {
|
|
|
156
157
|
return this.get<MarketSourcesResponse>(SKILL_HUB_API.market)
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
/** Add a market source repo; resolves with the fresh list. */
|
|
160
|
-
addMarketSource(repo: string): Promise<
|
|
161
|
-
return this.post<
|
|
160
|
+
/** Add a market source repo; resolves with the fresh list plus the resolved slug. */
|
|
161
|
+
addMarketSource(repo: string): Promise<MarketSourceAddResponse> {
|
|
162
|
+
return this.post<MarketSourceAddResponse>(SKILL_HUB_API.marketSource, { repo } satisfies MarketSourceRequest)
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/** Remove a market source repo; resolves with the fresh list. */
|
|
@@ -75,8 +75,10 @@ export function useMarketFlow(
|
|
|
75
75
|
setNewSourceName('')
|
|
76
76
|
setRepoResult(null)
|
|
77
77
|
setRepoSelected(new Set())
|
|
78
|
-
//
|
|
79
|
-
|
|
78
|
+
// 用服务端回传的归一化 slug 扫描(URL/owner@repo 输入都能对上记录)。
|
|
79
|
+
// 不能猜 `repos` 的位置:重复添加一个已存在的源只改它的 ref、不重排,
|
|
80
|
+
// 取末位会扫到另一个源。
|
|
81
|
+
const slug = result.repo
|
|
80
82
|
setScanningRepo(slug)
|
|
81
83
|
setRepoDiscoverState({ status: 'scanning' })
|
|
82
84
|
try {
|
|
@@ -10,6 +10,7 @@ import { useState, type ReactElement } from 'react'
|
|
|
10
10
|
import { IconChevronDownOutline14 } from './icons.tsx'
|
|
11
11
|
import type { HubKey } from './locales.ts'
|
|
12
12
|
import type { CardShell } from './settings-form.ts'
|
|
13
|
+
import { HEX_COLOR_RE } from '../protocol.ts'
|
|
13
14
|
import css from './settings-card.module.css'
|
|
14
15
|
|
|
15
16
|
/** Card-level chrome props. */
|
|
@@ -178,7 +179,7 @@ export interface ColorFieldProps extends FieldBaseProps {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
export function ColorField(props: ColorFieldProps): ReactElement {
|
|
181
|
-
const value =
|
|
182
|
+
const value = HEX_COLOR_RE.test(props.text) ? props.text : props.defaultColor
|
|
182
183
|
return (
|
|
183
184
|
<div className={css.field}>
|
|
184
185
|
<FieldShell {...props} />
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
13
|
+
// One definition of what a valid color/token looks like, shared with the host
|
|
14
|
+
// routes that re-validate the same drafts.
|
|
15
|
+
import { GITHUB_TOKEN_RE, HEX_COLOR_RE } from '../protocol.ts'
|
|
13
16
|
|
|
14
17
|
/** One staged field: its draft text plus the clear/reset marker. */
|
|
15
18
|
interface StagedEdit {
|
|
@@ -41,9 +44,9 @@ export function booleanField(field: string): FieldSpec {
|
|
|
41
44
|
export function colorField(field: string): FieldSpec {
|
|
42
45
|
return {
|
|
43
46
|
field,
|
|
44
|
-
format: (value) => (typeof value === 'string' &&
|
|
47
|
+
format: (value) => (typeof value === 'string' && HEX_COLOR_RE.test(value) ? value : ''),
|
|
45
48
|
parse: (text) => {
|
|
46
|
-
if (
|
|
49
|
+
if (HEX_COLOR_RE.test(text)) return { kind: 'set', value: text }
|
|
47
50
|
return undefined
|
|
48
51
|
},
|
|
49
52
|
}
|
|
@@ -61,7 +64,7 @@ export function secretField(field: string): FieldSpec {
|
|
|
61
64
|
parse: (text) => {
|
|
62
65
|
const trimmed = text.trim()
|
|
63
66
|
if (trimmed === '') return undefined
|
|
64
|
-
if (
|
|
67
|
+
if (!GITHUB_TOKEN_RE.test(trimmed)) return undefined
|
|
65
68
|
return { kind: 'set', value: trimmed }
|
|
66
69
|
},
|
|
67
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { SkillHubProvider } from './provider.ts'
|
|
|
24
24
|
import { makeRoutes } from './routes.ts'
|
|
25
25
|
import { createSkillStatsReader, asPersistenceSeam, type SessionPersistenceLike, type SessionQueryLike, type SkillStatsReader } from './stats.ts'
|
|
26
26
|
import { SkillHubStore } from './store.ts'
|
|
27
|
-
import { reconcileDisabledSkills } from './reconcile.ts'
|
|
27
|
+
import { reconcileDisabledSkills, pruneStaleDisabledSkills } from './reconcile.ts'
|
|
28
28
|
import { cleanupLeftoverImportDirs, setGithubToken } from './repo.ts'
|
|
29
29
|
import { dshHome } from './store.ts'
|
|
30
30
|
import { join } from 'node:path'
|
|
@@ -163,7 +163,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
163
163
|
}
|
|
164
164
|
return out as Partial<HubConfig>
|
|
165
165
|
}
|
|
166
|
-
const current = (): HubConfig => resolveHubConfig(
|
|
166
|
+
const current = (): HubConfig => resolveHubConfig(readConfig())
|
|
167
167
|
|
|
168
168
|
/** The Settings service, when this deployment mounts it. Absent ⇒ no user layer to read or write. */
|
|
169
169
|
const settingsOf = (): SettingsForms | undefined => ctx.get('settings') as SettingsForms | undefined
|
|
@@ -307,6 +307,16 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
307
307
|
} catch (error) {
|
|
308
308
|
ctx.logger.warn('[dsh-skill-hub] startup disabled-skill reconcile failed', error)
|
|
309
309
|
}
|
|
310
|
+
// 反向对账:记录还在、文件已不在 → 修路径或删记录。不做这一步,那条记录会
|
|
311
|
+
// 把名字永久锁死(启用/新建/删除三条路都失败)。
|
|
312
|
+
try {
|
|
313
|
+
const pruned = await pruneStaleDisabledSkills(store, home)
|
|
314
|
+
if (pruned.repaired.length > 0 || pruned.removed.length > 0) {
|
|
315
|
+
ctx.logger.info(`[dsh-skill-hub] startup pruned disabled records: repaired ${pruned.repaired.length} (${pruned.repaired.map((entry) => entry.name).join(', ') || '-'}), removed ${pruned.removed.length} (${pruned.removed.join(', ') || '-'})`)
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
ctx.logger.warn('[dsh-skill-hub] startup disabled-record prune failed', error)
|
|
319
|
+
}
|
|
310
320
|
})()
|
|
311
321
|
|
|
312
322
|
void (async () => {
|