@deepseek-ai/dsh-plugin-market-index 0.0.0-stage → 0.2.1-alpha.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/util/plugin-market-index/README.md
5
+ /:
6
+ en: be1a45173d5c1ce7
7
+ zh: a9a5a976e40e4a8a
8
+ /dsh-plugin-market-index:
9
+ en: 78d8cc75e40d4e48
10
+ zh: 9db1a93fb07d4ded
11
+ /dsh-plugin-market-index/summary:
12
+ en: f68c94d63c1ec0e5
13
+ zh: 52db4114eef7454d
14
+ /dsh-plugin-market-index/table-of-contents:
15
+ en: cd2a60a905c978d6
16
+ zh: cc71d387d0cbd442
17
+ /dsh-plugin-market-index/use-this-package:
18
+ en: 8eac2625e2e366a8
19
+ zh: 652131f240e929e0
20
+ /dsh-plugin-market-index/api:
21
+ en: e0e4444616427bf2
22
+ zh: 3960a7602bb17705
23
+ /dsh-plugin-market-index/model-experience:
24
+ en: c060b1659cf43243
25
+ zh: 4a2313822283291e
26
+ /dsh-plugin-market-index/model-experience/kv-cache-effect:
27
+ en: a7a5484352ac021c
28
+ zh: fe00da8797e85e06
29
+ /dsh-plugin-market-index/known-limitations-and-deferred-work:
30
+ en: acfbb04ae4682367
31
+ zh: 48af36f974976528
32
+ /dsh-plugin-market-index/known-limitations-and-deferred-work/dev-note:
33
+ en: e67f633dc6fe7773
34
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "Plugin market index format 1 validation and install arguments for DSH readers and index generators."
3
+ kind: "package-library"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # dsh-plugin-market-index
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Validator without runtime dependencies for format 1 of the plugin market index, the single JSON file that lists installable DSH bundles with their display metadata. [The format Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md) defines the document; this package is its executable rule set. DSH's [`dsh-plugin-market`](../../boot/plugin-market/README.md) reads indexes with it, and index generators validate their output with it before publishing.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [API](#api)
18
+ - [Model Experience](#model-experience)
19
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
20
+ - [Dev Note](#dev-note)
21
+
22
+ -----
23
+
24
+ <a id="use-this-package"></a>
25
+ ## Use this package
26
+
27
+ It is a library, not a service or plugin: no `ctx`, no registrations, no state, no I/O.
28
+
29
+ Parse the fetched document as JSON, then pass the value and the URL it was read from to `parseMarketIndex`. The first violated rule throws `MarketIndexError` with the JSON path of the offending value. A successful result drops members the format does not define, fills absent `metadata` groups, `keywords`, `labels`, `sortKeys`, and `engines` with empty values, orders each plugin's `versions` by descending SemVer precedence, and replaces each relative `icon` reference with its absolute URL.
30
+
31
+ Install one version with `marketInstallTarget(name, version)`: it returns the exact pnpm spec, `name@version` for npm and a commit-pinned `git+https` URL with an optional `path:` parameter for Git, and the registry to ask first when the version names one.
32
+
33
+ -----
34
+
35
+ <a id="api"></a>
36
+ ## API
37
+
38
+ ```ts
39
+ import { MarketIndexError, marketInstallTarget, parseMarketIndex } from '@deepseek-ai/dsh-plugin-market-index'
40
+ ```
41
+
42
+ | Export | Role |
43
+ |---|---|
44
+ | `parseMarketIndex(value, indexUrl)` | Validated `MarketIndex`; throws `MarketIndexError` for the first violated rule. |
45
+ | `marketInstallTarget(name, version)` | `{ spec, registry? }` that installs that exact version. |
46
+ | `MarketIndexError` | Validation failure carrying `path` and `reason`. |
47
+ | `MAX_INLINE_ICON_BYTES` | Largest decoded payload of an inline `data:` icon, 32 KiB. |
48
+ | `MarketIndex`, `MarketPlugin`, `MarketVersion`, `MarketEngines`, `MarketSource`, `MarketRepository`, `MarketIndexMetadata`, `MarketPluginMetadata`, `MarketDeclaration`, `MarketSortKeyDeclaration`, `MarketInstallTarget` | Validated record types. |
49
+
50
+ <a id="model-experience"></a>
51
+ ## Model Experience
52
+
53
+ Indirectly, through `dsh-plugin-market`, whose `plugin_market` tool returns validated plugins to the model.
54
+
55
+ #### KV Cache effect
56
+
57
+ None of its own. The tool result that carries plugins owns its request's cache behavior.
58
+
59
+ ## Known Limitations and Deferred Work
60
+
61
+ <a id="known-limitations-and-deferred-work"></a>
62
+
63
+ - **Generator obligations are unchecked** — the validator cannot verify that `name`, `version`, `publishTimestamp`, `engines.dsh`, or display fields match the package they describe, or that the package declares a bundle.
64
+ - **No size bound** — the caller bounds the document before parsing it.
65
+
66
+ <a id="dev-note"></a>
67
+ ### Dev Note
68
+
69
+ <details>
70
+ <summary>Working context for maintainers — click to expand</summary>
71
+
72
+ None.
73
+
74
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,74 @@
1
+ ---
2
+ description: "供 DSH 读取器和索引生成器使用的插件市场索引格式 1 校验与安装参数。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # dsh-plugin-market-index
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 插件市场索引格式 1 的校验器,没有运行时依赖。插件市场索引是一份 JSON 文件,列出可安装的 DSH 组合包(bundle)及其展示元数据。[格式 Agent Note](../../../.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.zh.md) 定义该文档;本包是其可执行的规则集。DSH 的 [`dsh-plugin-market`](../../boot/plugin-market/README.zh.md) 用它读取索引,索引生成器在发布前用它校验输出。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [API](#api)
18
+ - [模型体验](#model-experience)
19
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
20
+ - [开发备注](#dev-note)
21
+
22
+ -----
23
+
24
+ <a id="use-this-package"></a>
25
+ ## 使用本包
26
+
27
+ 本包是库,不是服务或插件:没有 `ctx`、没有注册、没有状态,也没有 I/O。
28
+
29
+ 先把获取到的文档解析为 JSON,再把该值和读取它的 URL 传给 `parseMarketIndex`。遇到第一条被违反的规则时,它抛出 `MarketIndexError`,并附带违规值的 JSON 路径。成功的结果会丢弃格式未定义的成员,用空值填充缺失的 `metadata` 分组、`keywords`、`labels`、`sortKeys` 和 `engines`,按 SemVer 优先级降序排列每个插件的 `versions`,并把每个相对 `icon` 引用替换为其绝对 URL。
30
+
31
+ 使用 `marketInstallTarget(name, version)` 安装一个版本:它返回精确的 pnpm spec(npm 为 `name@version`,Git 为固定到提交、可带 `path:` 参数的 `git+https` URL),该版本指定了 registry 时还返回首先询问的 registry。
32
+
33
+ -----
34
+
35
+ <a id="api"></a>
36
+ ## API
37
+
38
+ ```ts
39
+ import { MarketIndexError, marketInstallTarget, parseMarketIndex } from '@deepseek-ai/dsh-plugin-market-index'
40
+ ```
41
+
42
+ | 导出 | 作用 |
43
+ |---|---|
44
+ | `parseMarketIndex(value, indexUrl)` | 经过校验的 `MarketIndex`;遇到第一条被违反的规则时抛出 `MarketIndexError`。 |
45
+ | `marketInstallTarget(name, version)` | 安装该精确版本的 `{ spec, registry? }`。 |
46
+ | `MarketIndexError` | 携带 `path` 和 `reason` 的校验失败。 |
47
+ | `MAX_INLINE_ICON_BYTES` | 内联 `data:` 图标解码后载荷的最大字节数,即 32 KiB。 |
48
+ | `MarketIndex`, `MarketPlugin`, `MarketVersion`, `MarketEngines`, `MarketSource`, `MarketRepository`, `MarketIndexMetadata`, `MarketPluginMetadata`, `MarketDeclaration`, `MarketSortKeyDeclaration`, `MarketInstallTarget` | 经过校验的记录类型。 |
49
+
50
+ <a id="model-experience"></a>
51
+ ## 模型体验
52
+
53
+ 间接存在:通过 `dsh-plugin-market`,其 `plugin_market` 工具把经过校验的插件返回给模型。
54
+
55
+ #### KV Cache 影响
56
+
57
+ 本包自身没有。携带插件的工具结果决定其请求的缓存行为。
58
+
59
+ ## 已知限制与延期工作
60
+
61
+ <a id="known-limitations-and-deferred-work"></a>
62
+
63
+ - **不检查生成器义务**——校验器无法验证 `name`、`version`、`publishTimestamp`、`engines.dsh` 或展示字段是否与其描述的包一致,也无法验证该包是否声明了组合包。
64
+ - **没有大小上限**——调用方在解析前限制文档大小。
65
+
66
+ <a id="dev-note"></a>
67
+ ### 开发备注
68
+
69
+ <details>
70
+ <summary>维护者的工作上下文——点击展开</summary>
71
+
72
+ 无。
73
+
74
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,320 @@
1
+ import semver from "semver";
2
+ //#region lib/types/index.js
3
+ /**
4
+ * Plugin market index format 1: JSON validation, relative icon resolution, and install arguments.
5
+ * The format is defined by the plugin market index format Agent Note; readers and generators share this validator.
6
+ * @module @deepseek-ai/dsh-plugin-market-index
7
+ */
8
+ /** An index that violates format 1; `path` is the JSON path of the offending value, such as `$.plugins[0].versions[1].version`. */
9
+ var MarketIndexError = class extends Error {
10
+ path;
11
+ reason;
12
+ /**
13
+ * @param path - JSON path of the offending value.
14
+ * @param reason - the violated rule.
15
+ */
16
+ constructor(path, reason) {
17
+ super(`${path} ${reason}`);
18
+ this.path = path;
19
+ this.reason = reason;
20
+ this.name = "MarketIndexError";
21
+ }
22
+ };
23
+ const LANGUAGE_ID = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/u;
24
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u;
25
+ const PACKAGE_NAME_MAX_LENGTH = 214;
26
+ /** The SemVer 2.0.0 grammar for one exact version. */
27
+ const NUMERIC = "(?:0|[1-9]\\d*)";
28
+ const PRERELEASE_PART = `(?:${NUMERIC}|\\d*[a-zA-Z-][0-9a-zA-Z-]*)`;
29
+ const SEMVER = new RegExp(`^${NUMERIC}\\.${NUMERIC}\\.${NUMERIC}(?:-${PRERELEASE_PART}(?:\\.${PRERELEASE_PART})*)?(?:\\+[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*)?$`, "u");
30
+ const COMMIT = /^[0-9a-f]{40}$/u;
31
+ const PATH_SEGMENT = /^[A-Za-z0-9._@+-]+$/u;
32
+ const DATA_ICON = /^data:(?:image\/svg\+xml|image\/png|image\/jpeg|image\/webp);base64,([A-Za-z0-9+/]*={0,2})$/u;
33
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
34
+ /** Largest decoded payload of an inline `data:` icon, in bytes. */
35
+ const MAX_INLINE_ICON_BYTES = 32 * 1024;
36
+ const MAX_PUBLISH_TIMESTAMP = 1e11;
37
+ function fail(path, reason) {
38
+ throw new MarketIndexError(path, reason);
39
+ }
40
+ function objectAt(value, path) {
41
+ if (typeof value !== "object" || value === null || Array.isArray(value)) fail(path, "must be an object");
42
+ return value;
43
+ }
44
+ function stringAt(value, path) {
45
+ if (typeof value !== "string" || value.trim() === "") fail(path, "must be a string with a non-whitespace character");
46
+ return value;
47
+ }
48
+ function optional(object, key, path, read) {
49
+ return Object.hasOwn(object, key) ? read(object[key], `${path}.${key}`) : void 0;
50
+ }
51
+ function localizedText(value, path) {
52
+ if (typeof value === "string") return stringAt(value, path);
53
+ const object = objectAt(value, path);
54
+ const text = {};
55
+ for (const [language, member] of Object.entries(object)) {
56
+ if (!LANGUAGE_ID.test(language)) fail(`${path}.${language}`, "must be keyed by a lowercase language id");
57
+ own(text, language, stringAt(member, `${path}.${language}`));
58
+ }
59
+ if (!Object.hasOwn(text, "en")) fail(path, "must contain en");
60
+ return text;
61
+ }
62
+ /** Define an own enumerable member, so an id such as `__proto__` stays an ordinary key. */
63
+ function own(record, key, value) {
64
+ Object.defineProperty(record, key, {
65
+ value,
66
+ enumerable: true,
67
+ writable: true,
68
+ configurable: true
69
+ });
70
+ }
71
+ function declarations(value, path, read) {
72
+ const result = {};
73
+ for (const [id, member] of Object.entries(objectAt(value, path))) {
74
+ if (id === "") fail(path, "must not declare an empty id");
75
+ own(result, id, read(objectAt(member, `${path}.${id}`), `${path}.${id}`));
76
+ }
77
+ return result;
78
+ }
79
+ function declaration(object, path) {
80
+ return { title: localizedText(object.title, `${path}.title`) };
81
+ }
82
+ function sortKeyDeclaration(object, path) {
83
+ if (object.order !== "asc" && object.order !== "desc") fail(`${path}.order`, "must be \"asc\" or \"desc\"");
84
+ return {
85
+ title: localizedText(object.title, `${path}.title`),
86
+ order: object.order
87
+ };
88
+ }
89
+ function indexMetadata(value, path) {
90
+ const object = value === void 0 ? {} : objectAt(value, path);
91
+ return {
92
+ categories: optional(object, "categories", path, (member, at) => declarations(member, at, declaration)) ?? {},
93
+ labels: optional(object, "labels", path, (member, at) => declarations(member, at, declaration)) ?? {},
94
+ sortKeys: optional(object, "sortKeys", path, (member, at) => declarations(member, at, sortKeyDeclaration)) ?? {}
95
+ };
96
+ }
97
+ function declaredId(value, path, declared) {
98
+ const id = stringAt(value, path);
99
+ if (!Object.hasOwn(declared, id)) fail(path, `references undeclared id ${JSON.stringify(id)}`);
100
+ return id;
101
+ }
102
+ function pluginMetadata(value, path, index) {
103
+ const object = value === void 0 ? {} : objectAt(value, path);
104
+ const category = optional(object, "category", path, (member, at) => declaredId(member, at, index.categories));
105
+ const keywords = optional(object, "keywords", path, (member, at) => {
106
+ if (!Array.isArray(member)) fail(at, "must be an array");
107
+ return member.map((keyword, position) => {
108
+ if (typeof keyword !== "string") fail(`${at}[${String(position)}]`, "must be a string");
109
+ return keyword;
110
+ });
111
+ }) ?? [];
112
+ const labels = optional(object, "labels", path, (member, at) => {
113
+ if (!Array.isArray(member)) fail(at, "must be an array");
114
+ const ids = member.map((label, position) => declaredId(label, `${at}[${String(position)}]`, index.labels));
115
+ if (new Set(ids).size !== ids.length) fail(at, "must not repeat a label");
116
+ return ids;
117
+ }) ?? [];
118
+ const sortKeys = optional(object, "sortKeys", path, (member, at) => {
119
+ const values = {};
120
+ for (const [id, sortValue] of Object.entries(objectAt(member, at))) {
121
+ declaredId(id, `${at}.${id}`, index.sortKeys);
122
+ if (typeof sortValue !== "number" || !Number.isFinite(sortValue)) fail(`${at}.${id}`, "must be a finite number");
123
+ own(values, id, sortValue);
124
+ }
125
+ return values;
126
+ }) ?? {};
127
+ return {
128
+ ...category === void 0 ? {} : { category },
129
+ keywords,
130
+ labels,
131
+ sortKeys
132
+ };
133
+ }
134
+ function httpUrl(value, path, protocols) {
135
+ const text = stringAt(value, path);
136
+ let url;
137
+ try {
138
+ url = new URL(text);
139
+ } catch {
140
+ fail(path, "must be an absolute URL");
141
+ }
142
+ if (!protocols.includes(url.protocol)) fail(path, `must use ${protocols.join(" or ")}`);
143
+ if (url.username !== "" || url.password !== "") fail(path, "must not contain credentials");
144
+ if (url.href.includes("?") || url.href.includes("#")) fail(path, "must not contain a query or fragment");
145
+ return url;
146
+ }
147
+ /** An absolute http(s) URL; npm `homepage` values often carry a `#readme` fragment, so fragments and queries are kept. */
148
+ function homepageUrl(value, path) {
149
+ const text = stringAt(value, path);
150
+ if (!URL.canParse(text)) fail(path, "must be an absolute URL");
151
+ const url = new URL(text);
152
+ if (url.protocol !== "https:" && url.protocol !== "http:") fail(path, "must use https: or http:");
153
+ return url.href;
154
+ }
155
+ function gitUrl(value, path) {
156
+ const url = httpUrl(value, path, ["https:"]);
157
+ if (url.href !== value) fail(path, "must be a canonical URL");
158
+ if (!/^(?:\/[^/]+){2,}$/u.test(url.pathname)) fail(path, "must name a repository with at least two path segments");
159
+ if (/\.(?:tgz|tar\.gz)$/iu.test(url.pathname)) fail(path, "must name a repository, not a tarball");
160
+ return url.href;
161
+ }
162
+ function packagePath(value, path) {
163
+ const text = stringAt(value, path);
164
+ if (text.split("/").some((segment) => segment === "." || segment === ".." || !PATH_SEGMENT.test(segment))) fail(path, "must be a relative directory of letters, digits, and ._@+- segments without . or ..");
165
+ return text;
166
+ }
167
+ function repository(value, path) {
168
+ const object = objectAt(value, path);
169
+ const url = httpUrl(object.url, `${path}.url`, ["https:", "http:"]).href;
170
+ const directory = optional(object, "directory", path, packagePath);
171
+ return {
172
+ url,
173
+ ...directory === void 0 ? {} : { directory }
174
+ };
175
+ }
176
+ function source(value, path) {
177
+ const object = objectAt(value, path);
178
+ switch (object.type) {
179
+ case "npm": {
180
+ const registry = optional(object, "registry", path, (member, at) => httpUrl(member, at, ["https:", "http:"]).href);
181
+ return {
182
+ type: "npm",
183
+ ...registry === void 0 ? {} : { registry }
184
+ };
185
+ }
186
+ case "git": {
187
+ const commit = stringAt(object.commit, `${path}.commit`);
188
+ if (!COMMIT.test(commit)) fail(`${path}.commit`, "must be a lowercase 40-hex-digit commit SHA");
189
+ const packageDir = optional(object, "path", path, packagePath);
190
+ return {
191
+ type: "git",
192
+ url: gitUrl(object.url, `${path}.url`),
193
+ commit,
194
+ ...packageDir === void 0 ? {} : { path: packageDir }
195
+ };
196
+ }
197
+ default: return fail(`${path}.type`, "must be \"npm\" or \"git\"");
198
+ }
199
+ }
200
+ function engines(value, path) {
201
+ const dsh = optional(objectAt(value, path), "dsh", path, (member, at) => {
202
+ const range = stringAt(member, at);
203
+ if (range.startsWith("workspace:")) fail(at, "must not be a workspace: range");
204
+ if (semver.validRange(range) === null) fail(at, "must be a SemVer range");
205
+ return range;
206
+ });
207
+ return dsh === void 0 ? {} : { dsh };
208
+ }
209
+ function icon(value, path, indexUrl) {
210
+ const text = stringAt(value, path);
211
+ if (text.startsWith("data:")) {
212
+ const payload = DATA_ICON.exec(text)?.[1];
213
+ if (payload === void 0 || payload === "" || !BASE64.test(payload)) fail(path, "must be a base64 SVG, PNG, JPEG, or WebP data URL");
214
+ if (payload.length / 4 * 3 - (payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0) > 32768) fail(path, `must not exceed ${String(MAX_INLINE_ICON_BYTES / 1024)} KiB of decoded bytes`);
215
+ return text;
216
+ }
217
+ let resolved;
218
+ try {
219
+ resolved = new URL(text, indexUrl);
220
+ } catch {
221
+ fail(path, "must be a URL reference");
222
+ }
223
+ if (resolved.protocol !== "https:" && (resolved.origin === "null" || resolved.origin !== indexUrl.origin)) fail(path, "must resolve to https or the index origin");
224
+ return resolved.href;
225
+ }
226
+ function marketVersion(value, path) {
227
+ const object = objectAt(value, path);
228
+ const version = stringAt(object.version, `${path}.version`);
229
+ if (!SEMVER.test(version)) fail(`${path}.version`, "must be an exact SemVer version");
230
+ const publishTimestamp = object.publishTimestamp;
231
+ if (typeof publishTimestamp !== "number" || !Number.isInteger(publishTimestamp) || publishTimestamp < 0 || publishTimestamp >= MAX_PUBLISH_TIMESTAMP) fail(`${path}.publishTimestamp`, "must be an integer number of Unix seconds in [0, 10^11)");
232
+ return {
233
+ version,
234
+ publishTimestamp,
235
+ source: source(object.source, `${path}.source`),
236
+ engines: optional(object, "engines", path, engines) ?? {}
237
+ };
238
+ }
239
+ function versions(value, path) {
240
+ if (!Array.isArray(value) || value.length === 0) fail(path, "must be a non-empty array");
241
+ const seen = /* @__PURE__ */ new Set();
242
+ return value.map((member, position) => {
243
+ const at = `${path}[${String(position)}]`;
244
+ const entry = marketVersion(member, at);
245
+ if (seen.has(entry.version)) fail(`${at}.version`, `duplicates ${entry.version}`);
246
+ seen.add(entry.version);
247
+ return entry;
248
+ }).sort((left, right) => semver.rcompare(left.version, right.version));
249
+ }
250
+ function plugin(value, path, index, indexUrl) {
251
+ const object = objectAt(value, path);
252
+ const name = stringAt(object.name, `${path}.name`);
253
+ if (name.length > PACKAGE_NAME_MAX_LENGTH || !PACKAGE_NAME.test(name)) fail(`${path}.name`, "must be a valid npm package name");
254
+ const title = optional(object, "title", path, localizedText);
255
+ const description = optional(object, "description", path, localizedText);
256
+ const iconUrl = optional(object, "icon", path, (member, at) => icon(member, at, indexUrl));
257
+ const author = optional(object, "author", path, stringAt);
258
+ const homepage = optional(object, "homepage", path, homepageUrl);
259
+ const repo = optional(object, "repository", path, repository);
260
+ return {
261
+ name,
262
+ ...title === void 0 ? {} : { title },
263
+ ...description === void 0 ? {} : { description },
264
+ ...iconUrl === void 0 ? {} : { icon: iconUrl },
265
+ ...author === void 0 ? {} : { author },
266
+ ...homepage === void 0 ? {} : { homepage },
267
+ ...repo === void 0 ? {} : { repository: repo },
268
+ metadata: pluginMetadata(object.metadata, `${path}.metadata`, index),
269
+ versions: versions(object.versions, `${path}.versions`)
270
+ };
271
+ }
272
+ /**
273
+ * Validate a parsed format-1 index. Members the format does not define are ignored and dropped.
274
+ * @param value - the parsed JSON document.
275
+ * @param indexUrl - absolute URL the index was read from; relative icon references resolve against it.
276
+ * @returns the validated index with absent metadata groups and lists filled with empty values, and each plugin's
277
+ * versions in descending SemVer precedence.
278
+ * @throws {MarketIndexError} for the first violated rule.
279
+ */
280
+ function parseMarketIndex(value, indexUrl) {
281
+ if (!URL.canParse(indexUrl)) fail("$", `index URL ${JSON.stringify(indexUrl)} must be an absolute URL`);
282
+ const base = new URL(indexUrl);
283
+ const object = objectAt(value, "$");
284
+ if (object.format !== 1) fail("$.format", "must be 1");
285
+ const metadata = indexMetadata(object.metadata, "$.metadata");
286
+ if (!Array.isArray(object.plugins)) fail("$.plugins", "must be an array");
287
+ const names = /* @__PURE__ */ new Set();
288
+ const plugins = object.plugins.map((entry, position) => {
289
+ const parsed = plugin(entry, `$.plugins[${String(position)}]`, metadata, base);
290
+ if (names.has(parsed.name)) fail(`$.plugins[${String(position)}].name`, `duplicates ${parsed.name}`);
291
+ names.add(parsed.name);
292
+ return parsed;
293
+ });
294
+ const title = localizedText(object.title, "$.title");
295
+ const description = optional(object, "description", "$", localizedText);
296
+ return {
297
+ format: 1,
298
+ title,
299
+ ...description === void 0 ? {} : { description },
300
+ metadata,
301
+ plugins
302
+ };
303
+ }
304
+ /**
305
+ * Build the arguments that install one exact version: `name@version` for npm, and a `git+https` URL pinned to the
306
+ * commit, with pnpm's `path:` parameter for a package directory, for Git.
307
+ * @param name - the plugin's package name.
308
+ * @param version - a validated version of that plugin.
309
+ * @returns the install spec and the registry to ask first, when the version names one.
310
+ */
311
+ function marketInstallTarget(name, version) {
312
+ const { source: from } = version;
313
+ if (from.type === "npm") return {
314
+ spec: `${name}@${version.version}`,
315
+ ...from.registry === void 0 ? {} : { registry: from.registry }
316
+ };
317
+ return { spec: `git+${from.url.endsWith(".git") ? from.url : `${from.url}.git`}#${from.commit}${from.path === void 0 ? "" : `&path:/${from.path}`}` };
318
+ }
319
+ //#endregion
320
+ export { MAX_INLINE_ICON_BYTES, MarketIndexError, marketInstallTarget, parseMarketIndex };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Plugin market index format 1: JSON validation, relative icon resolution, and install arguments.
3
+ * The format is defined by the plugin market index format Agent Note; readers and generators share this validator.
4
+ * @module @deepseek-ai/dsh-plugin-market-index
5
+ */
6
+ import type { MarketIndex, MarketInstallTarget, MarketVersion } from './types.ts';
7
+ export type * from './types.ts';
8
+ /** An index that violates format 1; `path` is the JSON path of the offending value, such as `$.plugins[0].versions[1].version`. */
9
+ export declare class MarketIndexError extends Error {
10
+ readonly path: string;
11
+ readonly reason: string;
12
+ /**
13
+ * @param path - JSON path of the offending value.
14
+ * @param reason - the violated rule.
15
+ */
16
+ constructor(path: string, reason: string);
17
+ }
18
+ /** Largest decoded payload of an inline `data:` icon, in bytes. */
19
+ export declare const MAX_INLINE_ICON_BYTES: number;
20
+ /**
21
+ * Validate a parsed format-1 index. Members the format does not define are ignored and dropped.
22
+ * @param value - the parsed JSON document.
23
+ * @param indexUrl - absolute URL the index was read from; relative icon references resolve against it.
24
+ * @returns the validated index with absent metadata groups and lists filled with empty values, and each plugin's
25
+ * versions in descending SemVer precedence.
26
+ * @throws {MarketIndexError} for the first violated rule.
27
+ */
28
+ export declare function parseMarketIndex(value: unknown, indexUrl: string): MarketIndex;
29
+ /**
30
+ * Build the arguments that install one exact version: `name@version` for npm, and a `git+https` URL pinned to the
31
+ * commit, with pnpm's `path:` parameter for a package directory, for Git.
32
+ * @param name - the plugin's package name.
33
+ * @param version - a validated version of that plugin.
34
+ * @returns the install spec and the registry to ask first, when the version names one.
35
+ */
36
+ export declare function marketInstallTarget(name: string, version: Pick<MarketVersion, 'version' | 'source'>): MarketInstallTarget;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Validated plugin market index records.
3
+ * @module @deepseek-ai/dsh-plugin-market-index/types
4
+ */
5
+ import type { LocalizedText } from '@deepseek-ai/dsh-package-manifest';
6
+ /** A market-declared category or label: an opaque id's display title. */
7
+ export interface MarketDeclaration {
8
+ readonly title: LocalizedText;
9
+ }
10
+ /** A market-declared sort key: its display title and initial direction. */
11
+ export interface MarketSortKeyDeclaration {
12
+ readonly title: LocalizedText;
13
+ readonly order: 'asc' | 'desc';
14
+ }
15
+ /** Market-defined declarations; absent groups are empty records. */
16
+ export interface MarketIndexMetadata {
17
+ readonly categories: Readonly<Record<string, MarketDeclaration>>;
18
+ readonly labels: Readonly<Record<string, MarketDeclaration>>;
19
+ readonly sortKeys: Readonly<Record<string, MarketSortKeyDeclaration>>;
20
+ }
21
+ /** Market-defined values of one plugin; category, label, and sort-key ids require index declarations. */
22
+ export interface MarketPluginMetadata {
23
+ readonly category?: string;
24
+ /** Free-form strings, conventionally from package keywords; absent input becomes an empty array. */
25
+ readonly keywords: readonly string[];
26
+ readonly labels: readonly string[];
27
+ readonly sortKeys: Readonly<Record<string, number>>;
28
+ }
29
+ /** Where a plugin's source code is published; display only, never an install source. */
30
+ export interface MarketRepository {
31
+ /** Absolute http(s) URL of the repository's web page, without credentials, query, or fragment. */
32
+ readonly url: string;
33
+ /** Relative POSIX directory of the package inside the repository; absent means the repository root. */
34
+ readonly directory?: string;
35
+ }
36
+ /** Where one exact version is installed from. */
37
+ export type MarketSource = {
38
+ readonly type: 'npm';
39
+ /** http(s) registry URL; absent means the installing client's configured registry. */
40
+ readonly registry?: string;
41
+ } | {
42
+ readonly type: 'git';
43
+ /** Canonical https repository URL without credentials, query, or fragment. */
44
+ readonly url: string;
45
+ /** Lowercase 40-hex-digit commit SHA. */
46
+ readonly commit: string;
47
+ /** Relative POSIX package directory; absent means the repository root. */
48
+ readonly path?: string;
49
+ };
50
+ /** Runtime requirements of one version. */
51
+ export interface MarketEngines {
52
+ /** SemVer range of DSH versions the version runs on; absent means any DSH version. */
53
+ readonly dsh?: string;
54
+ }
55
+ /** One installable version of a plugin. */
56
+ export interface MarketVersion {
57
+ /** Exact SemVer version, unique within its plugin. */
58
+ readonly version: string;
59
+ /** Publish time of `version` in Unix seconds. */
60
+ readonly publishTimestamp: number;
61
+ readonly source: MarketSource;
62
+ readonly engines: MarketEngines;
63
+ }
64
+ /** One installable bundle and its listed versions. */
65
+ export interface MarketPlugin {
66
+ /** npm package name, unique within its index. */
67
+ readonly name: string;
68
+ readonly title?: LocalizedText;
69
+ readonly description?: LocalizedText;
70
+ /** Base64 `data:` URL or absolute URL; a relative index reference is already resolved against the index URL. */
71
+ readonly icon?: string;
72
+ readonly author?: string;
73
+ /** Absolute http(s) URL of a page about the plugin. */
74
+ readonly homepage?: string;
75
+ /** Web page of the plugin's source repository and its package directory; display only. */
76
+ readonly repository?: MarketRepository;
77
+ readonly metadata: MarketPluginMetadata;
78
+ /** Non-empty, in descending SemVer precedence. */
79
+ readonly versions: readonly MarketVersion[];
80
+ }
81
+ /** One validated format-1 index; members the format does not define are dropped. */
82
+ export interface MarketIndex {
83
+ readonly format: 1;
84
+ readonly title: LocalizedText;
85
+ readonly description?: LocalizedText;
86
+ readonly metadata: MarketIndexMetadata;
87
+ /** Plugins in the index's default order. */
88
+ readonly plugins: readonly MarketPlugin[];
89
+ }
90
+ /** The arguments that install one exact version. */
91
+ export interface MarketInstallTarget {
92
+ /** Install spec accepted by `pnpm add`. */
93
+ readonly spec: string;
94
+ /** Registry to ask first; absent means the installing client's configured registry. */
95
+ readonly registry?: string;
96
+ }
97
+ //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-plugin-market-index",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Plugin market index format 1: JSON validation, relative icon resolution, and pinned install arguments",
4
+ "version": "0.2.1-alpha.4",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/util/plugin-market-index"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@deepseek-ai/dsh-package-manifest": "0.2.1-alpha.4",
31
+ "semver": "^7.8.5"
32
+ },
33
+ "peerDependencies": {
34
+ "@deepseek-ai/cordis": "~4.0.5-alpha.2"
35
+ },
36
+ "devDependencies": {
37
+ "@deepseek-ai/cordis": "~4.0.5-alpha.2",
38
+ "@types/semver": "^7.8.0"
39
+ }
6
40
  }