relay-dsh-plugin-manager 0.2.4 → 0.2.6
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/CHANGELOG.md +27 -0
- package/README.md +38 -3
- package/README.zh.md +31 -3
- package/SPEC.md +1 -0
- package/docs/acceptance.md +1 -0
- package/lib/index.d.ts +19 -1
- package/lib/index.js +209 -2
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.6] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add the operator-only `RELAY_PLUGIN_MANAGER_TELEMETRY_TEST=1` acceptance mode.
|
|
10
|
+
Its schema `1.1.0` events carry only `is_test: true`, allowing growth dashboards
|
|
11
|
+
to exclude production acceptance without changing ordinary anonymous telemetry.
|
|
12
|
+
|
|
13
|
+
### Compatibility
|
|
14
|
+
|
|
15
|
+
- Ordinary users remain on default-on anonymous telemetry with the same event
|
|
16
|
+
fields and opt-out. The Registry continues accepting legacy schema `1.0.0`.
|
|
17
|
+
|
|
18
|
+
## [0.2.5] - 2026-09-07
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Default-on anonymous operational analytics for plugin-manager use and
|
|
23
|
+
manager-executed install start, success, and failure outcomes, with explicit
|
|
24
|
+
opt-out through `RELAY_PLUGIN_MANAGER_TELEMETRY=0`.
|
|
25
|
+
- First-party delivery to the Registry's versioned telemetry endpoint; the npm
|
|
26
|
+
package contains no PostHog host or project key.
|
|
27
|
+
- A locally persisted random anonymous identifier; search text, installed
|
|
28
|
+
inventory, local paths, command output, error messages, IP geolocation, and
|
|
29
|
+
account identity are excluded.
|
|
30
|
+
|
|
5
31
|
## [0.2.4] - 2026-09-05
|
|
6
32
|
|
|
7
33
|
### Changed
|
|
@@ -130,3 +156,4 @@ All notable changes to this project are documented here.
|
|
|
130
156
|
[0.1.0]: https://github.com/yangbobo2021/relay-dsh-plugin-manager/compare/v0.1.0-rc.4...v0.1.0
|
|
131
157
|
[0.1.1]: https://github.com/yangbobo2021/relay-dsh-plugin-manager/compare/v0.1.0...v0.1.1
|
|
132
158
|
[0.2.3]: https://github.com/yangbobo2021/relay-dsh-plugin-manager/compare/v0.2.2...v0.2.3
|
|
159
|
+
[0.2.6]: https://github.com/yangbobo2021/relay-dsh-plugin-manager/compare/v0.2.5...v0.2.6
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Manage DeepSeek Harness Plugins from Chat
|
|
2
2
|
|
|
3
|
-
> **Now supports DSH `0.1.2-rc.1` while retaining `0.1.2-alpha.3` compatibility.**
|
|
3
|
+
> **Now supports DSH `0.1.2-rc.1` while retaining `0.1.2-alpha.3` compatibility.** The stable channel includes the evidence-backed DSH Registry search provider and anonymous operational analytics. [Install it from npm](https://www.npmjs.com/package/relay-dsh-plugin-manager) · [Compatibility evidence](https://github.com/yangbobo2021/Relay/tree/codex/relay-foundation/dsh-lab/dsh-0.1.2-rc.1-20260903).
|
|
4
4
|
|
|
5
|
-
> **Release channel:** `latest`
|
|
5
|
+
> **Release channel:** install `relay-dsh-plugin-manager@latest` to receive the current stable manager.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-manager@
|
|
8
|
+
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-manager@latest
|
|
9
9
|
npx @deepseek-ai/dsh@0.1.2-rc.1 web
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -100,6 +100,41 @@ missing companion peers, waits for one confirmation, and runs the installs in
|
|
|
100
100
|
sequence. Completed changes that still need restart are reported explicitly as
|
|
101
101
|
restart-required or waiting for a manual restart.
|
|
102
102
|
|
|
103
|
+
### Anonymous usage analytics
|
|
104
|
+
|
|
105
|
+
Anonymous operational analytics are enabled by default to measure plugin-manager
|
|
106
|
+
search and installation outcomes. To opt out without disabling plugin management,
|
|
107
|
+
set this when starting DSH Web:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
RELAY_PLUGIN_MANAGER_TELEMETRY=0 dsh web
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Registry operators performing a production acceptance run can mark only that
|
|
114
|
+
run as test traffic:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
RELAY_PLUGIN_MANAGER_TELEMETRY_TEST=1 dsh web
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This adds only `is_test: true`; ordinary users do not send the marker.
|
|
121
|
+
|
|
122
|
+
The manager sends events only to the first-party
|
|
123
|
+
`https://dsh-plugins.tech/v1/telemetry/events` endpoint. The Registry validates
|
|
124
|
+
the exact event schema and forwards accepted events to its PostHog analytics
|
|
125
|
+
project. The plugin records only the manager action type and plugin
|
|
126
|
+
install start, success, or failure events produced by the manager. Install
|
|
127
|
+
events contain the public package name, activation/restart booleans, and a
|
|
128
|
+
bounded error code. Search terms, conversation content, local paths, command
|
|
129
|
+
output, error text, IP geolocation, and account identity are not sent. A random
|
|
130
|
+
anonymous ID is stored in `.relay-plugin-manager/telemetry.json` inside the
|
|
131
|
+
current Profile to avoid counting one installation as many users; delete the
|
|
132
|
+
file to reset it.
|
|
133
|
+
|
|
134
|
+
These events measure manager use and manager-executed installs. They do not
|
|
135
|
+
claim that another plugin's feature was actually used merely because it loaded
|
|
136
|
+
or activated.
|
|
137
|
+
|
|
103
138
|
### DSH Registry discovery provider
|
|
104
139
|
|
|
105
140
|
The source-only Registry search provider uses `https://dsh-plugins.tech` by
|
package/README.zh.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# 在 DeepSeek Harness 对话中管理插件
|
|
2
2
|
|
|
3
|
-
> **现已支持 DSH `0.1.2-rc.1`,并保留对 `0.1.2-alpha.3` 的兼容。**
|
|
3
|
+
> **现已支持 DSH `0.1.2-rc.1`,并保留对 `0.1.2-alpha.3` 的兼容。** 稳定通道已接入有证据边界的 DSH Registry 搜索和匿名运行统计。[从 npm 安装](https://www.npmjs.com/package/relay-dsh-plugin-manager) · [兼容性证据](https://github.com/yangbobo2021/Relay/tree/codex/relay-foundation/dsh-lab/dsh-0.1.2-rc.1-20260903)。
|
|
4
4
|
|
|
5
|
-
> **发布通道:** `latest`
|
|
5
|
+
> **发布通道:** 安装 `relay-dsh-plugin-manager@latest` 即可获得当前稳定版管理器。
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-manager@
|
|
8
|
+
npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add relay-dsh-plugin-manager@latest
|
|
9
9
|
npx @deepseek-ai/dsh@0.1.2-rc.1 web
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -95,6 +95,34 @@ dsh web
|
|
|
95
95
|
一次确认后依次安装。修改完成但仍需重启时,状态会明确区分“需要另行重启”和
|
|
96
96
|
“等待人工重启”。
|
|
97
97
|
|
|
98
|
+
### 匿名使用统计
|
|
99
|
+
|
|
100
|
+
匿名运行统计默认开启,用于衡量插件管理器的搜索和安装结果。如需关闭,可在启动
|
|
101
|
+
DSH Web 时设置以下变量;关闭统计不会影响插件管理功能:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
RELAY_PLUGIN_MANAGER_TELEMETRY=0 dsh web
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Registry 运营方执行生产验收时,可以只标记该次运行:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
RELAY_PLUGIN_MANAGER_TELEMETRY_TEST=1 dsh web
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
该模式只增加 `is_test: true`;普通用户不会发送这个标记。
|
|
114
|
+
|
|
115
|
+
管理器只向第一方接口 `https://dsh-plugins.tech/v1/telemetry/events` 发送事件;Registry
|
|
116
|
+
会校验严格的事件结构,再把通过校验的事件转发到自己的 PostHog 项目。统计只记录
|
|
117
|
+
管理器操作类型,以及由管理器执行的插件安装开始、成功或失败;安装
|
|
118
|
+
事件包含公开的插件包名、是否激活、是否需要重启和有限错误代码。不会发送搜索词、
|
|
119
|
+
对话内容、本地路径、命令输出、错误文本、IP 地理位置或登录身份。一个随机匿名 ID
|
|
120
|
+
保存在当前 Profile 的 `.relay-plugin-manager/telemetry.json`,用于避免把同一安装重复
|
|
121
|
+
计算为多个用户;删除该文件即可重置。
|
|
122
|
+
|
|
123
|
+
这些数据只能说明“插件管理器被使用”和“管理器执行了安装”,不会把插件加载或
|
|
124
|
+
激活推断成其他插件的真实功能使用。
|
|
125
|
+
|
|
98
126
|
### DSH Registry 发现数据源
|
|
99
127
|
|
|
100
128
|
来源级 Registry 搜索 provider 默认使用 `https://dsh-plugins.tech`。可设置插件
|
package/SPEC.md
CHANGED
|
@@ -51,6 +51,7 @@ service.
|
|
|
51
51
|
| PM-024 | GitHub owner discovery recognizes `owner:<name>`, owner-only GitHub identities, `<name> DSH plugins`, and conservative bare identifiers containing digits. It sends a typed owner intent to providers, uses GitHub's exact owner qualifier, case-insensitively verifies returned ownership, and ranks verified owner matches first. Owner-only `inspect` fails with an actionable error directing callers to search. Every emitted repository identity is accepted by `inspect`; every recommended immutable source is accepted by `plan`. |
|
|
52
52
|
| PM-025 | Register a read-only DSH Registry provider against `https://dsh-plugins.tech` by default, with an explicit `registryUrl: false` opt-out and HTTPS-only configuration override. Send only the task query, inferred Chinese/English locale and result limit; accept only source-level untrusted DiscoveryEntry records, convert their npm/GitHub descriptors into core-owned source types, and submit them to the same mandatory local inspection used by all providers. Registry responses cannot supply exact versions, approval, plans, installers, or profile state, and provider failure remains isolated. |
|
|
53
53
|
| PM-026 | Return search candidates as one explicit, one-based relevance-ranked result page with a default and maximum size of 20. The model-facing contract requires the Agent to preserve rank order, surface every possibly relevant returned candidate, exclude candidates whose purpose is clearly unrelated, and MUST NOT silently reduce the remaining page to a fixed top-N. Ranking remains distinct from compatibility, security, or installation approval. |
|
|
54
|
+
| PM-027 | Send default-on anonymous operational telemetry only to the Registry's canonical first-party HTTPS endpoint under schema `1.1.0`, with explicit `RELAY_PLUGIN_MANAGER_TELEMETRY=0` or `telemetry.enabled: false` opt-out. Events are limited to manager discovery/planning type and manager-executed install start/success/failure, including per-plugin batch outcomes. An operator-only `RELAY_PLUGIN_MANAGER_TELEMETRY_TEST=1` or `telemetry.test: true` mode adds only `is_test: true`; ordinary events omit the marker. Never send raw search text, conversation content, Profile state, installed inventory, paths, credentials, command output, error text, account identity, PostHog configuration, or client timestamps. Store only a stable random anonymous ID locally, create it lazily, and never let storage, validation, network, Registry, or analytics failure affect plugin management. |
|
|
54
55
|
|
|
55
56
|
## Command Grammar
|
|
56
57
|
|
package/docs/acceptance.md
CHANGED
|
@@ -39,6 +39,7 @@ This matrix is the release contract. Tests name the relevant `PM-*` ids.
|
|
|
39
39
|
| A-031 search results feed one immutable multi-install plan through the real DSH tool and user-question registries; no mutation starts before one exact UI approval, then child installs execute serially and approval replay is rejected | PM-002, PM-005, PM-009, PM-010, PM-015, PM-023, PM-024 | combined host/tool/search/question/manager integration |
|
|
40
40
|
| A-032 The default formal Registry searches at least 3,000 source records for Chinese task text, English task text, exact plugin names and an honest empty result; every returned source is locally inspected to an immutable npm version or GitHub commit, while Registry authority remains source-only, untrusted, non-recommending and non-approving | PM-005, PM-006, PM-025 | provider/unit/host integration tests plus `npm run acceptance:live:registry` |
|
|
41
41
|
| A-033 Search defaults to a complete twenty-candidate result page, assigns stable one-based display ranks after merge and deduplication, and tells the Agent to exclude clearly unrelated purposes while surfacing every remaining possibly relevant candidate without silent fixed-count truncation | PM-005, PM-026 | search orchestration and conversation-tool contract unit tests plus Agent answer acceptance |
|
|
42
|
+
| A-034 Default manager actions send only schema-valid anonymous envelopes to the first-party Registry endpoint, reuse a lazily created random ID, record each attempted batch install outcome, and continue normally through storage or delivery failure; explicit opt-out creates no ID and makes no telemetry request | PM-027 | telemetry unit tests + manager integration event assertions + Registry endpoint contract and end-to-end PostHog verification |
|
|
42
43
|
|
|
43
44
|
## Test Layers
|
|
44
45
|
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,20 @@ import { a as PluginSearchRuntime, c as PluginSource, i as PluginSearchRequest,
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
4
|
|
|
5
|
+
//#region src/telemetry.d.ts
|
|
6
|
+
type TelemetryProperty = string | number | boolean;
|
|
7
|
+
interface Telemetry {
|
|
8
|
+
capture(event: string, properties?: Readonly<Record<string, TelemetryProperty>>): void;
|
|
9
|
+
}
|
|
10
|
+
interface TelemetryConfig {
|
|
11
|
+
/** Anonymous operational telemetry is enabled unless this is explicitly false. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Registry telemetry endpoint. Only the canonical service or localhost is accepted. */
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
/** Marks an operator-controlled acceptance run so analytics can exclude it. */
|
|
16
|
+
test?: boolean;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
5
19
|
//#region src/profile.d.ts
|
|
6
20
|
interface PackageSurface {
|
|
7
21
|
packageName: string;
|
|
@@ -238,6 +252,7 @@ interface PluginManagerDependencies {
|
|
|
238
252
|
operations?: OperationTracker;
|
|
239
253
|
fetchOptions?: Omit<FetchOptions, 'signal'>;
|
|
240
254
|
hmrTimeoutMs?: number;
|
|
255
|
+
telemetry?: Telemetry;
|
|
241
256
|
}
|
|
242
257
|
interface DiscoverRequest {
|
|
243
258
|
action: 'list' | 'search' | 'inspect' | 'status';
|
|
@@ -301,7 +316,9 @@ declare class PluginManager {
|
|
|
301
316
|
private readonly operations;
|
|
302
317
|
private readonly fetchOptions;
|
|
303
318
|
private readonly hmrTimeoutMs;
|
|
319
|
+
private readonly telemetry;
|
|
304
320
|
constructor(dependencies: PluginManagerDependencies);
|
|
321
|
+
private capture;
|
|
305
322
|
private loaderEntries;
|
|
306
323
|
list(): PluginStatus[];
|
|
307
324
|
discover(request: DiscoverRequest, signal?: AbortSignal): Promise<unknown>;
|
|
@@ -331,8 +348,9 @@ declare const DEFAULT_REGISTRY_ORIGIN = "https://dsh-plugins.tech";
|
|
|
331
348
|
interface Config {
|
|
332
349
|
allowRestart?: boolean;
|
|
333
350
|
registryUrl?: string | false;
|
|
351
|
+
telemetry?: TelemetryConfig;
|
|
334
352
|
}
|
|
335
353
|
declare function apply(ctx: Context, config?: Config): void;
|
|
336
354
|
//#endregion
|
|
337
|
-
export { Config, DEFAULT_REGISTRY_ORIGIN, type DiscoverRequest, type InstallManyItemResult, type InstallManyItemStatus, type InstallManyResult, type MutationResult, type PlanRequest, type PluginInspection, PluginManager, type PluginSearchCandidate, type PluginSearchMatch, type PluginSearchProvider, type PluginSearchRequest, type PluginSource, apply, inject, name };
|
|
355
|
+
export { Config, DEFAULT_REGISTRY_ORIGIN, type DiscoverRequest, type InstallManyItemResult, type InstallManyItemStatus, type InstallManyResult, type MutationResult, type PlanRequest, type PluginInspection, PluginManager, type PluginSearchCandidate, type PluginSearchMatch, type PluginSearchProvider, type PluginSearchRequest, type PluginSource, type TelemetryConfig, apply, inject, name };
|
|
338
356
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -1223,11 +1223,56 @@ const PROTECTED_ENTRY_IDS = /* @__PURE__ */ new Set([
|
|
|
1223
1223
|
"webserver",
|
|
1224
1224
|
"web-runtime"
|
|
1225
1225
|
]);
|
|
1226
|
+
const TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1227
|
+
"INVALID_SOURCE",
|
|
1228
|
+
"INVALID_NPM_SPEC",
|
|
1229
|
+
"INVALID_NPM_VERSION",
|
|
1230
|
+
"INVALID_GITHUB_SPEC",
|
|
1231
|
+
"INVALID_GITHUB_REF",
|
|
1232
|
+
"GITHUB_OWNER_REQUIRES_SEARCH",
|
|
1233
|
+
"IMMUTABLE_SOURCE_REQUIRED",
|
|
1234
|
+
"NETWORK_ERROR",
|
|
1235
|
+
"SOURCE_HTTP_ERROR",
|
|
1236
|
+
"INVALID_SOURCE_METADATA",
|
|
1237
|
+
"INVALID_PLUGIN_MANIFEST",
|
|
1238
|
+
"NOT_DSH_PLUGIN",
|
|
1239
|
+
"PACKAGE_NAME_MISMATCH",
|
|
1240
|
+
"NPM_INTEGRITY_MISSING",
|
|
1241
|
+
"INVALID_SEARCH_QUERY",
|
|
1242
|
+
"INVALID_ACTION",
|
|
1243
|
+
"INVALID_BATCH",
|
|
1244
|
+
"DUPLICATE_SEARCH_PROVIDER",
|
|
1245
|
+
"PROFILE_READ_FAILED",
|
|
1246
|
+
"PROFILE_WRITE_FAILED",
|
|
1247
|
+
"PLUGIN_NOT_INSTALLED",
|
|
1248
|
+
"PLUGIN_ALREADY_INSTALLED",
|
|
1249
|
+
"ENABLEMENT_UNSUPPORTED",
|
|
1250
|
+
"ENABLEMENT_CONFLICT",
|
|
1251
|
+
"PROTECTED_PLUGIN",
|
|
1252
|
+
"CONFIRMATION_REQUIRED",
|
|
1253
|
+
"CONFIRMATION_INVALID",
|
|
1254
|
+
"CONFIRMATION_EXPIRED",
|
|
1255
|
+
"CONFIRMATION_REPLAYED",
|
|
1256
|
+
"PLAN_STALE",
|
|
1257
|
+
"OPERATION_NOT_FOUND",
|
|
1258
|
+
"DSH_COMMAND_FAILED",
|
|
1259
|
+
"BATCH_INSTALL_FAILED",
|
|
1260
|
+
"POSTCONDITION_FAILED",
|
|
1261
|
+
"RESTART_UNAVAILABLE"
|
|
1262
|
+
]);
|
|
1226
1263
|
function safePackageName(value) {
|
|
1227
1264
|
const name = value?.trim() ?? "";
|
|
1228
1265
|
if (!NPM_NAME.test(name)) fail("INVALID_NPM_SPEC", "A valid installed package name is required.");
|
|
1229
1266
|
return name;
|
|
1230
1267
|
}
|
|
1268
|
+
function queryLengthBucket(value) {
|
|
1269
|
+
const length = value?.trim().length ?? 0;
|
|
1270
|
+
if (length === 0) return "empty";
|
|
1271
|
+
if (length <= 10) return "1-10";
|
|
1272
|
+
if (length <= 30) return "11-30";
|
|
1273
|
+
if (length <= 80) return "31-80";
|
|
1274
|
+
return "81+";
|
|
1275
|
+
}
|
|
1231
1276
|
function commandResult(result) {
|
|
1232
1277
|
return {
|
|
1233
1278
|
exitCode: result.exitCode,
|
|
@@ -1241,6 +1286,10 @@ function operationError(error) {
|
|
|
1241
1286
|
message: error instanceof Error ? error.message : String(error)
|
|
1242
1287
|
};
|
|
1243
1288
|
}
|
|
1289
|
+
function telemetryErrorCode(error) {
|
|
1290
|
+
const code = operationError(error).code;
|
|
1291
|
+
return code !== void 0 && TELEMETRY_ERROR_CODES.has(code) ? code : "UNKNOWN";
|
|
1292
|
+
}
|
|
1244
1293
|
function installedPackageManifest(profileDir, packageName) {
|
|
1245
1294
|
try {
|
|
1246
1295
|
return JSON.parse(readFileSync(join(profileDir, "node_modules", packageName, "package.json"), "utf8"));
|
|
@@ -1265,6 +1314,7 @@ var PluginManager = class {
|
|
|
1265
1314
|
operations;
|
|
1266
1315
|
fetchOptions;
|
|
1267
1316
|
hmrTimeoutMs;
|
|
1317
|
+
telemetry;
|
|
1268
1318
|
constructor(dependencies) {
|
|
1269
1319
|
this.profileDir = dependencies.profileDir;
|
|
1270
1320
|
this.searchRuntime = dependencies.searchRuntime;
|
|
@@ -1277,6 +1327,12 @@ var PluginManager = class {
|
|
|
1277
1327
|
this.operations = dependencies.operations ?? new OperationTracker();
|
|
1278
1328
|
this.fetchOptions = dependencies.fetchOptions ?? {};
|
|
1279
1329
|
this.hmrTimeoutMs = dependencies.hmrTimeoutMs ?? 5e3;
|
|
1330
|
+
this.telemetry = dependencies.telemetry ?? { capture() {} };
|
|
1331
|
+
}
|
|
1332
|
+
capture(event, properties = {}) {
|
|
1333
|
+
try {
|
|
1334
|
+
this.telemetry.capture(event, properties);
|
|
1335
|
+
} catch {}
|
|
1280
1336
|
}
|
|
1281
1337
|
loaderEntries() {
|
|
1282
1338
|
if (this.loader === void 0) return [];
|
|
@@ -1297,6 +1353,14 @@ var PluginManager = class {
|
|
|
1297
1353
|
return listPluginStatuses(this.profileDir, this.loaderEntries());
|
|
1298
1354
|
}
|
|
1299
1355
|
async discover(request, signal) {
|
|
1356
|
+
this.capture("plugin_manager_used", {
|
|
1357
|
+
surface: "discover",
|
|
1358
|
+
action: request.action,
|
|
1359
|
+
...request.action === "search" ? {
|
|
1360
|
+
has_query: (request.query?.trim().length ?? 0) > 0,
|
|
1361
|
+
query_length_bucket: queryLengthBucket(request.query)
|
|
1362
|
+
} : {}
|
|
1363
|
+
});
|
|
1300
1364
|
if (request.action === "list") return {
|
|
1301
1365
|
profile: "web",
|
|
1302
1366
|
plugins: this.list()
|
|
@@ -1360,6 +1424,11 @@ var PluginManager = class {
|
|
|
1360
1424
|
});
|
|
1361
1425
|
}
|
|
1362
1426
|
async plan(request, signal) {
|
|
1427
|
+
this.capture("plugin_manager_used", {
|
|
1428
|
+
surface: "plan",
|
|
1429
|
+
action: request.operation,
|
|
1430
|
+
...request.operation === "install_many" ? { batch_size: request.sources?.length ?? 0 } : {}
|
|
1431
|
+
});
|
|
1363
1432
|
if (request.operation === "install_many") return await this.planInstallMany(request.sources, signal);
|
|
1364
1433
|
if (request.operation === "restart") {
|
|
1365
1434
|
if (!this.restarter.available()) fail("RESTART_UNAVAILABLE", "Automatic restart is unavailable in this deployment.");
|
|
@@ -1486,7 +1555,22 @@ var PluginManager = class {
|
|
|
1486
1555
|
if (plan.packageName === void 0) fail("POSTCONDITION_FAILED", "Mutation plan has no package name.");
|
|
1487
1556
|
if (plan.action === "install" || plan.action === "update") {
|
|
1488
1557
|
if (plan.installSpec === void 0) fail("POSTCONDITION_FAILED", "Install/update plan has no immutable source.");
|
|
1489
|
-
|
|
1558
|
+
if (plan.action === "install") this.capture("plugin_install_started", { plugin_name: plan.packageName });
|
|
1559
|
+
try {
|
|
1560
|
+
const result = this.withRestartGuidance(await this.installOrUpdate(plan.action, plan.packageName, plan.installSpec, context), automaticRestartAvailable);
|
|
1561
|
+
if (plan.action === "install") this.capture("plugin_install_succeeded", {
|
|
1562
|
+
plugin_name: plan.packageName,
|
|
1563
|
+
activated: result.activated === true,
|
|
1564
|
+
restart_required: result.restartRequired
|
|
1565
|
+
});
|
|
1566
|
+
return result;
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
if (plan.action === "install") this.capture("plugin_install_failed", {
|
|
1569
|
+
plugin_name: plan.packageName,
|
|
1570
|
+
error_code: telemetryErrorCode(error)
|
|
1571
|
+
});
|
|
1572
|
+
throw error;
|
|
1573
|
+
}
|
|
1490
1574
|
}
|
|
1491
1575
|
if (plan.action === "remove") return this.withRestartGuidance(await this.remove(plan.packageName, context), automaticRestartAvailable);
|
|
1492
1576
|
return this.withRestartGuidance(await this.toggle(plan.action, plan.packageName, context), automaticRestartAvailable);
|
|
@@ -1534,6 +1618,10 @@ var PluginManager = class {
|
|
|
1534
1618
|
break;
|
|
1535
1619
|
}
|
|
1536
1620
|
context.progress(`install_many: ${index + 1}/${items.length} installing ${item.packageName}`);
|
|
1621
|
+
this.capture("plugin_install_started", {
|
|
1622
|
+
plugin_name: item.packageName,
|
|
1623
|
+
batch: true
|
|
1624
|
+
});
|
|
1537
1625
|
try {
|
|
1538
1626
|
const result = this.withRestartGuidance(await this.installOrUpdate("install", item.packageName, item.installSpec, context), automaticRestartAvailable);
|
|
1539
1627
|
results.push({
|
|
@@ -1542,6 +1630,12 @@ var PluginManager = class {
|
|
|
1542
1630
|
status: this.mutationCompletion(result, automaticRestartAvailable).status,
|
|
1543
1631
|
result
|
|
1544
1632
|
});
|
|
1633
|
+
this.capture("plugin_install_succeeded", {
|
|
1634
|
+
plugin_name: item.packageName,
|
|
1635
|
+
batch: true,
|
|
1636
|
+
activated: result.activated === true,
|
|
1637
|
+
restart_required: result.restartRequired
|
|
1638
|
+
});
|
|
1545
1639
|
} catch (error) {
|
|
1546
1640
|
results.push({
|
|
1547
1641
|
packageName: item.packageName,
|
|
@@ -1549,6 +1643,11 @@ var PluginManager = class {
|
|
|
1549
1643
|
status: context.signal.aborted ? "cancelled" : "failed",
|
|
1550
1644
|
error: operationError(error)
|
|
1551
1645
|
});
|
|
1646
|
+
this.capture("plugin_install_failed", {
|
|
1647
|
+
plugin_name: item.packageName,
|
|
1648
|
+
batch: true,
|
|
1649
|
+
error_code: telemetryErrorCode(error)
|
|
1650
|
+
});
|
|
1552
1651
|
skipRemaining(index + 1);
|
|
1553
1652
|
break;
|
|
1554
1653
|
}
|
|
@@ -2078,6 +2177,108 @@ var DshCliRunner = class {
|
|
|
2078
2177
|
}
|
|
2079
2178
|
};
|
|
2080
2179
|
//#endregion
|
|
2180
|
+
//#region src/telemetry.ts
|
|
2181
|
+
const DEFAULT_ENDPOINT = "https://dsh-plugins.tech/v1/telemetry/events";
|
|
2182
|
+
const STATE_DIRECTORY = ".relay-plugin-manager";
|
|
2183
|
+
const STATE_FILE = "telemetry.json";
|
|
2184
|
+
const SCHEMA_VERSION = "1.1.0";
|
|
2185
|
+
const EVENTS = /* @__PURE__ */ new Set([
|
|
2186
|
+
"plugin_manager_used",
|
|
2187
|
+
"plugin_install_started",
|
|
2188
|
+
"plugin_install_succeeded",
|
|
2189
|
+
"plugin_install_failed"
|
|
2190
|
+
]);
|
|
2191
|
+
const noopTelemetry = Object.freeze({ capture() {} });
|
|
2192
|
+
function safeEndpoint(value) {
|
|
2193
|
+
try {
|
|
2194
|
+
const parsed = new URL(value ?? DEFAULT_ENDPOINT);
|
|
2195
|
+
const local = [
|
|
2196
|
+
"localhost",
|
|
2197
|
+
"127.0.0.1",
|
|
2198
|
+
"::1"
|
|
2199
|
+
].includes(parsed.hostname);
|
|
2200
|
+
const canonical = parsed.protocol === "https:" && parsed.hostname === "dsh-plugins.tech";
|
|
2201
|
+
if (!local && !canonical || local && !["http:", "https:"].includes(parsed.protocol)) return null;
|
|
2202
|
+
if (parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/v1/telemetry/events" || parsed.search !== "" || parsed.hash !== "") return null;
|
|
2203
|
+
return parsed.href;
|
|
2204
|
+
} catch {
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
function anonymousId(profileDir, random) {
|
|
2209
|
+
const directory = join(profileDir, STATE_DIRECTORY);
|
|
2210
|
+
const path = join(directory, STATE_FILE);
|
|
2211
|
+
try {
|
|
2212
|
+
const existing = JSON.parse(readFileSync(path, "utf8"));
|
|
2213
|
+
if (typeof existing.anonymousId === "string" && /^[0-9a-f-]{36}$/iu.test(existing.anonymousId)) return existing.anonymousId;
|
|
2214
|
+
} catch {}
|
|
2215
|
+
const id = random();
|
|
2216
|
+
try {
|
|
2217
|
+
mkdirSync(directory, {
|
|
2218
|
+
recursive: true,
|
|
2219
|
+
mode: 448
|
|
2220
|
+
});
|
|
2221
|
+
writeFileSync(path, `${JSON.stringify({ anonymousId: id })}\n`, { mode: 384 });
|
|
2222
|
+
} catch {}
|
|
2223
|
+
return id;
|
|
2224
|
+
}
|
|
2225
|
+
function allowedProperties(event, properties) {
|
|
2226
|
+
const keys = new Set(Object.keys(properties));
|
|
2227
|
+
const exact = (required, optional = []) => {
|
|
2228
|
+
if (required.some((key) => !keys.has(key))) return false;
|
|
2229
|
+
return [...keys].every((key) => required.includes(key) || optional.includes(key));
|
|
2230
|
+
};
|
|
2231
|
+
if (event === "plugin_manager_used") {
|
|
2232
|
+
if (!exact(["surface", "action"], [
|
|
2233
|
+
"has_query",
|
|
2234
|
+
"query_length_bucket",
|
|
2235
|
+
"batch_size"
|
|
2236
|
+
])) return false;
|
|
2237
|
+
if (!["discover", "plan"].includes(String(properties.surface))) return false;
|
|
2238
|
+
return typeof properties.action === "string";
|
|
2239
|
+
}
|
|
2240
|
+
if (event === "plugin_install_started") return exact(["plugin_name"], ["batch"]);
|
|
2241
|
+
if (event === "plugin_install_succeeded") return exact([
|
|
2242
|
+
"plugin_name",
|
|
2243
|
+
"activated",
|
|
2244
|
+
"restart_required"
|
|
2245
|
+
], ["batch"]);
|
|
2246
|
+
if (event === "plugin_install_failed") return exact(["plugin_name", "error_code"], ["batch"]);
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
function createTelemetry(profileDir, config, runtime = {
|
|
2250
|
+
fetch,
|
|
2251
|
+
random: randomUUID
|
|
2252
|
+
}) {
|
|
2253
|
+
if (config?.enabled === false) return noopTelemetry;
|
|
2254
|
+
const endpoint = safeEndpoint(config?.endpoint);
|
|
2255
|
+
if (endpoint === null) return noopTelemetry;
|
|
2256
|
+
let distinctId;
|
|
2257
|
+
return Object.freeze({ capture(event, properties = {}) {
|
|
2258
|
+
if (!EVENTS.has(event) || !allowedProperties(event, properties)) return;
|
|
2259
|
+
distinctId ??= anonymousId(profileDir, runtime.random);
|
|
2260
|
+
const controller = new AbortController();
|
|
2261
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
2262
|
+
timeout.unref?.();
|
|
2263
|
+
try {
|
|
2264
|
+
runtime.fetch(endpoint, {
|
|
2265
|
+
method: "POST",
|
|
2266
|
+
headers: { "content-type": "application/json" },
|
|
2267
|
+
body: JSON.stringify({
|
|
2268
|
+
schema_version: SCHEMA_VERSION,
|
|
2269
|
+
anonymous_id: distinctId,
|
|
2270
|
+
event,
|
|
2271
|
+
properties,
|
|
2272
|
+
...config?.test === true ? { is_test: true } : {}
|
|
2273
|
+
}),
|
|
2274
|
+
signal: controller.signal
|
|
2275
|
+
}).catch(() => void 0).finally(() => clearTimeout(timeout));
|
|
2276
|
+
} catch {
|
|
2277
|
+
clearTimeout(timeout);
|
|
2278
|
+
}
|
|
2279
|
+
} });
|
|
2280
|
+
}
|
|
2281
|
+
//#endregion
|
|
2081
2282
|
//#region src/index.ts
|
|
2082
2283
|
const name = "relay-dsh-plugin-manager";
|
|
2083
2284
|
const inject = [
|
|
@@ -2090,6 +2291,11 @@ const inject = [
|
|
|
2090
2291
|
const DEFAULT_REGISTRY_ORIGIN = "https://dsh-plugins.tech";
|
|
2091
2292
|
function apply(ctx, config = {}) {
|
|
2092
2293
|
const profileDir = profileDirectory("web");
|
|
2294
|
+
const telemetry = config.telemetry ?? {
|
|
2295
|
+
enabled: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY !== "0",
|
|
2296
|
+
endpoint: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY_ENDPOINT,
|
|
2297
|
+
test: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY_TEST === "1"
|
|
2298
|
+
};
|
|
2093
2299
|
ctx.pluginSearch.register(npmSearchProvider());
|
|
2094
2300
|
ctx.pluginSearch.register(githubSearchProvider());
|
|
2095
2301
|
const configuredRegistryUrl = config.registryUrl ?? process.env.DSH_PLUGIN_REGISTRY_URL?.trim();
|
|
@@ -2101,7 +2307,8 @@ function apply(ctx, config = {}) {
|
|
|
2101
2307
|
runner: new DshCliRunner(),
|
|
2102
2308
|
hot: new HotRuntime(ctx, profileDir),
|
|
2103
2309
|
restarter: new DshRestarter({ allowRestart: config.allowRestart }),
|
|
2104
|
-
loader: ctx.loader
|
|
2310
|
+
loader: ctx.loader,
|
|
2311
|
+
telemetry: createTelemetry(profileDir, telemetry)
|
|
2105
2312
|
}));
|
|
2106
2313
|
}
|
|
2107
2314
|
//#endregion
|