dshmarket 1.5.1 → 1.6.0
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/README.md +18 -1
- package/README.zh.md +16 -1
- package/client/client.js +67 -67
- package/lib/dsh-cli.js +187 -24
- package/lib/index.js +35 -5
- package/lib/install.js +10 -8
- package/lib/profile.js +20 -14
- package/lib/routes.js +67 -51
- package/lib/themes.js +5 -5
- package/lib/types/dsh-cli.d.ts +34 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/install.d.ts +2 -2
- package/lib/types/profile.d.ts +12 -8
- package/lib/types/routes.d.ts +4 -1
- package/lib/types/themes.d.ts +1 -1
- package/lib/types/updates.d.ts +1 -1
- package/lib/types/verify.d.ts +1 -1
- package/lib/updates.js +9 -7
- package/lib/verify.js +12 -11
- package/package.json +1 -1
- package/src/dsh-cli.ts +234 -23
- package/src/index.ts +52 -8
- package/src/install.ts +10 -8
- package/src/profile.ts +19 -14
- package/src/routes.ts +76 -49
- package/src/themes.ts +10 -5
- package/src/updates.ts +15 -8
- package/src/verify.ts +12 -10
package/README.md
CHANGED
|
@@ -25,6 +25,23 @@ dsh plugin --profile web add dshmarket
|
|
|
25
25
|
|
|
26
26
|
Restart `dsh web`, then open **Settings → Plugin Market**.
|
|
27
27
|
|
|
28
|
+
### DSH Desktop
|
|
29
|
+
|
|
30
|
+
On DSH Desktop builds that expose the public `desktopProfiles` and
|
|
31
|
+
`desktopPnpm` services, install from **Open DSH Terminal** with:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin add dshmarket
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The market automatically targets the immutable active Desktop profile and
|
|
38
|
+
uses Desktop's packaged pnpm operation service. It does not probe or install a
|
|
39
|
+
system pnpm, and Desktop keeps ownership of application restart. Ordinary DSH
|
|
40
|
+
continues to use the existing profile/argv/CLI path when those services are
|
|
41
|
+
absent. The detection order follows Desktop's supported
|
|
42
|
+
[cross-environment plugin contract](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/4f68147091e585aaa1d815f99d30a657b3842d7c/dsh-plugin-desktop/docs/plugin-services.md#cross-environment-plugin-optional-desktop-adapter-and-ordinary-dsh-fallback).
|
|
43
|
+
This compatibility does not mean the market is bundled with Desktop.
|
|
44
|
+
|
|
28
45
|
## What you get
|
|
29
46
|
|
|
30
47
|
- **Browse & search** the full community catalog (300+ plugins, growing daily) — category filters, star counts, top/new sorting, bilingual descriptions that follow your UI language
|
|
@@ -68,7 +85,7 @@ Live from [awesome-dsh-plugin.com/plugins.json](https://awesome-dsh-plugin.com/p
|
|
|
68
85
|
|
|
69
86
|
### DeepSeek Harness Desktop
|
|
70
87
|
|
|
71
|
-
[DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop) is a modern desktop client for the DeepSeek Harness ecosystem — start and manage a local Harness service without
|
|
88
|
+
[DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop) is a modern desktop client for the DeepSeek Harness ecosystem — start and manage a local Harness service without a system Node.js installation. Compatible builds can host a user-installed dsh-market through their public profile and package-operation services; bundling remains a Desktop project decision.
|
|
72
89
|
|
|
73
90
|
[Website](https://www.dshdesktop.cn) · [GitHub](https://github.com/anywhere-labs/deepseek-harness-desktop)
|
|
74
91
|
|
package/README.zh.md
CHANGED
|
@@ -25,6 +25,21 @@ dsh plugin --profile web add dshmarket
|
|
|
25
25
|
|
|
26
26
|
重启 `dsh web`,打开 **设置 → 插件市场**。
|
|
27
27
|
|
|
28
|
+
### DSH Desktop
|
|
29
|
+
|
|
30
|
+
如果 DSH Desktop 版本提供公开的 `desktopProfiles` 与 `desktopPnpm`
|
|
31
|
+
服务,可从 **Open DSH Terminal** 安装:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin add dshmarket
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
市场会自动使用该代 Desktop 已锁定的活动 profile,以及 Desktop 内置的
|
|
38
|
+
pnpm 操作服务;不会探测或安装系统 pnpm,应用重启仍由 Desktop 负责。
|
|
39
|
+
没有这些服务的普通 DSH 会继续使用现有 profile/argv/CLI 路径。检测顺序遵循
|
|
40
|
+
Desktop 支持的[跨环境插件 contract](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/4f68147091e585aaa1d815f99d30a657b3842d7c/dsh-plugin-desktop/docs/plugin-services.zh.md#跨环境插件可选-desktop-adapter-与普通-dsh-fallback)。
|
|
41
|
+
兼容 Desktop 并不代表市场已预装或捆绑进 Desktop。
|
|
42
|
+
|
|
28
43
|
## 你会得到
|
|
29
44
|
|
|
30
45
|
- **逛与搜**——完整社区目录(300+ 插件,每天在涨),分类筛选、star 数、最热/最新排序,中英描述跟随界面语言
|
|
@@ -68,7 +83,7 @@ dsh plugin --profile web add dshmarket
|
|
|
68
83
|
|
|
69
84
|
### DeepSeek Harness Desktop
|
|
70
85
|
|
|
71
|
-
[DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop) 是一款为 DeepSeek Harness
|
|
86
|
+
[DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop) 是一款为 DeepSeek Harness 生态打造的现代化桌面端,无需在系统中安装 Node.js 即可启动和管理本地 Harness 服务。兼容版本可通过公开的 profile 与包操作服务运行用户自行安装的 dsh-market;是否预装仍由 Desktop 项目决定。
|
|
72
87
|
|
|
73
88
|
[访问官网](https://www.dshdesktop.cn) · [GitHub](https://github.com/anywhere-labs/deepseek-harness-desktop)
|
|
74
89
|
|
package/client/client.js
CHANGED
|
@@ -442,86 +442,86 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
442
442
|
document.head.appendChild(tag);
|
|
443
443
|
}
|
|
444
444
|
var Market_module_css_default = {
|
|
445
|
-
"
|
|
446
|
-
"
|
|
447
|
-
"
|
|
445
|
+
"swatches": "eGUBIq_swatches",
|
|
446
|
+
"body": "eGUBIq_body",
|
|
447
|
+
"okState": "eGUBIq_okState",
|
|
448
|
+
"empty": "eGUBIq_empty",
|
|
449
|
+
"desc": "eGUBIq_desc",
|
|
450
|
+
"modalNote": "eGUBIq_modalNote",
|
|
451
|
+
"dangerArmed": "eGUBIq_dangerArmed",
|
|
452
|
+
"sizeOn": "eGUBIq_sizeOn",
|
|
448
453
|
"filterTitle": "eGUBIq_filterTitle",
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
"
|
|
452
|
-
"
|
|
453
|
-
"
|
|
454
|
-
"
|
|
454
|
+
"warnLine": "eGUBIq_warnLine",
|
|
455
|
+
"actLive": "eGUBIq_actLive",
|
|
456
|
+
"cmdSummary": "eGUBIq_cmdSummary",
|
|
457
|
+
"foot": "eGUBIq_foot",
|
|
458
|
+
"filterBtnOn": "eGUBIq_filterBtnOn",
|
|
459
|
+
"grow": "eGUBIq_grow",
|
|
455
460
|
"actBroken": "eGUBIq_actBroken",
|
|
461
|
+
"act": "eGUBIq_act",
|
|
462
|
+
"pagerSize": "eGUBIq_pagerSize",
|
|
463
|
+
"pageOn": "eGUBIq_pageOn",
|
|
464
|
+
"barWave": "eGUBIq_barWave",
|
|
465
|
+
"tag": "eGUBIq_tag",
|
|
466
|
+
"head": "eGUBIq_head",
|
|
467
|
+
"searchInline": "eGUBIq_searchInline",
|
|
468
|
+
"tab": "eGUBIq_tab",
|
|
469
|
+
"filterBtn": "eGUBIq_filterBtn",
|
|
470
|
+
"catsWrap": "eGUBIq_catsWrap",
|
|
471
|
+
"catsToggle": "eGUBIq_catsToggle",
|
|
472
|
+
"title": "eGUBIq_title",
|
|
473
|
+
"themesGrid": "eGUBIq_themesGrid",
|
|
474
|
+
"nm": "eGUBIq_nm",
|
|
475
|
+
"pager": "eGUBIq_pager",
|
|
476
|
+
"irow": "eGUBIq_irow",
|
|
477
|
+
"pageBtn": "eGUBIq_pageBtn",
|
|
478
|
+
"staleAction": "eGUBIq_staleAction",
|
|
456
479
|
"pageInfo": "eGUBIq_pageInfo",
|
|
457
|
-
"
|
|
480
|
+
"err": "eGUBIq_err",
|
|
458
481
|
"catsCollapsed": "eGUBIq_catsCollapsed",
|
|
482
|
+
"actWarn": "eGUBIq_actWarn",
|
|
459
483
|
"actWhy": "eGUBIq_actWhy",
|
|
460
|
-
"loading": "eGUBIq_loading",
|
|
461
|
-
"sizeLabel": "eGUBIq_sizeLabel",
|
|
462
|
-
"cats": "eGUBIq_cats",
|
|
463
|
-
"bar": "eGUBIq_bar",
|
|
464
|
-
"pager": "eGUBIq_pager",
|
|
465
|
-
"titleRow": "eGUBIq_titleRow",
|
|
466
|
-
"on": "eGUBIq_on",
|
|
467
|
-
"filterOption": "eGUBIq_filterOption",
|
|
468
|
-
"dangerArmed": "eGUBIq_dangerArmed",
|
|
469
|
-
"sect": "eGUBIq_sect",
|
|
470
|
-
"pct": "eGUBIq_pct",
|
|
471
|
-
"cancelBtn": "eGUBIq_cancelBtn",
|
|
472
|
-
"pagerSize": "eGUBIq_pagerSize",
|
|
473
|
-
"dot": "eGUBIq_dot",
|
|
474
|
-
"restart": "eGUBIq_restart",
|
|
475
|
-
"sub": "eGUBIq_sub",
|
|
476
|
-
"top": "eGUBIq_top",
|
|
477
|
-
"catsRow": "eGUBIq_catsRow",
|
|
478
484
|
"src": "eGUBIq_src",
|
|
479
|
-
"
|
|
485
|
+
"card": "eGUBIq_card",
|
|
486
|
+
"star": "eGUBIq_star",
|
|
480
487
|
"dangerBtn": "eGUBIq_dangerBtn",
|
|
481
|
-
"
|
|
482
|
-
"barFill": "eGUBIq_barFill",
|
|
483
|
-
"tabs": "eGUBIq_tabs",
|
|
484
|
-
"sizeOn": "eGUBIq_sizeOn",
|
|
485
|
-
"modalNote": "eGUBIq_modalNote",
|
|
486
|
-
"av": "eGUBIq_av",
|
|
487
|
-
"grid": "eGUBIq_grid",
|
|
488
|
-
"warnBtn": "eGUBIq_warnBtn",
|
|
489
|
-
"desc": "eGUBIq_desc",
|
|
490
|
-
"cmdSummary": "eGUBIq_cmdSummary",
|
|
491
|
-
"dshmSlide": "eGUBIq_dshmSlide",
|
|
488
|
+
"pct": "eGUBIq_pct",
|
|
492
489
|
"filterGroup": "eGUBIq_filterGroup",
|
|
493
|
-
"irow": "eGUBIq_irow",
|
|
494
|
-
"foot": "eGUBIq_foot",
|
|
495
|
-
"row1": "eGUBIq_row1",
|
|
496
|
-
"filterPanel": "eGUBIq_filterPanel",
|
|
497
|
-
"nm": "eGUBIq_nm",
|
|
498
|
-
"spec": "eGUBIq_spec",
|
|
499
|
-
"spin": "eGUBIq_spin",
|
|
500
|
-
"card": "eGUBIq_card",
|
|
501
|
-
"pagerPages": "eGUBIq_pagerPages",
|
|
502
490
|
"pageEllipsis": "eGUBIq_pageEllipsis",
|
|
491
|
+
"grid": "eGUBIq_grid",
|
|
492
|
+
"dshmSlide": "eGUBIq_dshmSlide",
|
|
493
|
+
"av": "eGUBIq_av",
|
|
503
494
|
"sizeBtn": "eGUBIq_sizeBtn",
|
|
504
|
-
"head": "eGUBIq_head",
|
|
505
|
-
"catsWrap": "eGUBIq_catsWrap",
|
|
506
|
-
"tab": "eGUBIq_tab",
|
|
507
|
-
"themesGrid": "eGUBIq_themesGrid",
|
|
508
|
-
"grow": "eGUBIq_grow",
|
|
509
|
-
"progress": "eGUBIq_progress",
|
|
510
|
-
"swatches": "eGUBIq_swatches",
|
|
511
|
-
"owner": "eGUBIq_owner",
|
|
512
|
-
"title": "eGUBIq_title",
|
|
513
495
|
"root": "eGUBIq_root",
|
|
514
|
-
"searchInline": "eGUBIq_searchInline",
|
|
515
|
-
"warnLine": "eGUBIq_warnLine",
|
|
516
|
-
"empty": "eGUBIq_empty",
|
|
517
|
-
"filterBtn": "eGUBIq_filterBtn",
|
|
518
|
-
"pageOn": "eGUBIq_pageOn",
|
|
519
|
-
"body": "eGUBIq_body",
|
|
520
496
|
"filterWrap": "eGUBIq_filterWrap",
|
|
521
|
-
"
|
|
522
|
-
"
|
|
497
|
+
"pagerPages": "eGUBIq_pagerPages",
|
|
498
|
+
"row1": "eGUBIq_row1",
|
|
499
|
+
"owner": "eGUBIq_owner",
|
|
500
|
+
"warnBtn": "eGUBIq_warnBtn",
|
|
501
|
+
"tabs": "eGUBIq_tabs",
|
|
502
|
+
"titleRow": "eGUBIq_titleRow",
|
|
503
|
+
"barFill": "eGUBIq_barFill",
|
|
504
|
+
"spec": "eGUBIq_spec",
|
|
523
505
|
"cmdDetails": "eGUBIq_cmdDetails",
|
|
524
|
-
"
|
|
506
|
+
"top": "eGUBIq_top",
|
|
507
|
+
"sub": "eGUBIq_sub",
|
|
508
|
+
"restart": "eGUBIq_restart",
|
|
509
|
+
"descTight": "eGUBIq_descTight",
|
|
510
|
+
"progress": "eGUBIq_progress",
|
|
511
|
+
"cmd": "eGUBIq_cmd",
|
|
512
|
+
"bar": "eGUBIq_bar",
|
|
513
|
+
"cancelBtn": "eGUBIq_cancelBtn",
|
|
514
|
+
"sect": "eGUBIq_sect",
|
|
515
|
+
"spin": "eGUBIq_spin",
|
|
516
|
+
"filterPanel": "eGUBIq_filterPanel",
|
|
517
|
+
"dot": "eGUBIq_dot",
|
|
518
|
+
"catsRow": "eGUBIq_catsRow",
|
|
519
|
+
"sp": "eGUBIq_sp",
|
|
520
|
+
"loading": "eGUBIq_loading",
|
|
521
|
+
"on": "eGUBIq_on",
|
|
522
|
+
"cats": "eGUBIq_cats",
|
|
523
|
+
"filterOption": "eGUBIq_filterOption",
|
|
524
|
+
"sizeLabel": "eGUBIq_sizeLabel"
|
|
525
525
|
};
|
|
526
526
|
//#endregion
|
|
527
527
|
//#region src/client/MarketSection.tsx
|
package/lib/dsh-cli.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
-
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
12
12
|
import { logEvent } from './log.js';
|
|
13
13
|
import { createProgressTracker } from './ndjson.js';
|
|
14
14
|
import { pluginArgsFor } from './pnpm-compat.js';
|
|
@@ -75,6 +75,7 @@ export function killChild(child) {
|
|
|
75
75
|
/** The child of the operation currently running, for /dsh-market/cancel. */
|
|
76
76
|
let activeChild = null;
|
|
77
77
|
let cancelRequested = false;
|
|
78
|
+
let activeDesktopOperation = null;
|
|
78
79
|
/**
|
|
79
80
|
* Kill a child and its whole tree, gracefully where the platform allows:
|
|
80
81
|
* taskkill /T /F on Windows (plain kill() leaves pnpm children running),
|
|
@@ -115,6 +116,12 @@ function killTree(child) {
|
|
|
115
116
|
* @returns true when there was one to cancel.
|
|
116
117
|
*/
|
|
117
118
|
export function cancelActive() {
|
|
119
|
+
if (activeDesktopOperation !== null) {
|
|
120
|
+
activeDesktopOperation.userCancelled = true;
|
|
121
|
+
progress.cancelling = true;
|
|
122
|
+
activeDesktopOperation.cancel();
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
118
125
|
if (activeChild === null)
|
|
119
126
|
return false;
|
|
120
127
|
cancelRequested = true;
|
|
@@ -205,6 +212,34 @@ export const BOOT_ID = `${String(process.pid)}-${String(Date.now())}`;
|
|
|
205
212
|
const TARGET_RE = /^[A-Za-z0-9@:./_#+-]+$/;
|
|
206
213
|
/** Mutating pnpm commands get the structured reporter appended. */
|
|
207
214
|
const NDJSON_COMMANDS = new Set(['add', 'remove', 'install']);
|
|
215
|
+
/** Apply profile-specific pnpm compatibility and the structured reporter. */
|
|
216
|
+
function preparePluginArgs(profileDirectory, pluginArgs) {
|
|
217
|
+
let args = pluginArgsFor(profileDirectory, [...pluginArgs]);
|
|
218
|
+
const target = args[args.length - 1] ?? '';
|
|
219
|
+
if (!TARGET_RE.test(target)) {
|
|
220
|
+
return { error: `unsafe plugin target rejected: ${JSON.stringify(target)}` };
|
|
221
|
+
}
|
|
222
|
+
if (NDJSON_COMMANDS.has(args[0]))
|
|
223
|
+
args = [...args, '--reporter=ndjson'];
|
|
224
|
+
return { args, target };
|
|
225
|
+
}
|
|
226
|
+
/** Reset the singleton status snapshot before one operation starts. */
|
|
227
|
+
function beginProgress(target) {
|
|
228
|
+
progress.active = true;
|
|
229
|
+
progress.target = target;
|
|
230
|
+
progress.startedAt = Date.now();
|
|
231
|
+
progress.lastLine = '';
|
|
232
|
+
progress.phase = null;
|
|
233
|
+
progress.done = 0;
|
|
234
|
+
progress.total = null;
|
|
235
|
+
progress.currentPackage = null;
|
|
236
|
+
progress.downloaded = null;
|
|
237
|
+
progress.size = null;
|
|
238
|
+
progress.ndjson = false;
|
|
239
|
+
progress.error = null;
|
|
240
|
+
progress.cancelling = false;
|
|
241
|
+
return createProgressTracker();
|
|
242
|
+
}
|
|
208
243
|
/**
|
|
209
244
|
* Line-buffered progress feed: pnpm's ndjson reporter emits one JSON object
|
|
210
245
|
* per line on stdout, and chunk boundaries can split a line. Human fallback
|
|
@@ -231,30 +266,13 @@ function makeProgressFeeder(tracker) {
|
|
|
231
266
|
/** Run one `dsh plugin --profile <p> …` command with timeout and progress tracking. */
|
|
232
267
|
export function runDshPlugin(profile, pluginArgs) {
|
|
233
268
|
const { file, args, cwd, viaShell } = dshArgv();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return Promise.resolve({ exitCode: 1, timedOut: false, stdout: '', stderr: `unsafe plugin target rejected: ${JSON.stringify(target)}`, cancelled: false });
|
|
269
|
+
const prepared = preparePluginArgs(profileDir(profile), pluginArgs);
|
|
270
|
+
if ('error' in prepared) {
|
|
271
|
+
logEvent('error', 'install', prepared.error);
|
|
272
|
+
return Promise.resolve({ exitCode: 1, timedOut: false, stdout: '', stderr: prepared.error, cancelled: false });
|
|
239
273
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (NDJSON_COMMANDS.has(pluginArgs[0]))
|
|
243
|
-
pluginArgs = [...pluginArgs, '--reporter=ndjson'];
|
|
244
|
-
progress.active = true;
|
|
245
|
-
progress.target = target;
|
|
246
|
-
progress.startedAt = Date.now();
|
|
247
|
-
progress.lastLine = '';
|
|
248
|
-
progress.phase = null;
|
|
249
|
-
progress.done = 0;
|
|
250
|
-
progress.total = null;
|
|
251
|
-
progress.currentPackage = null;
|
|
252
|
-
progress.downloaded = null;
|
|
253
|
-
progress.size = null;
|
|
254
|
-
progress.ndjson = false;
|
|
255
|
-
progress.error = null;
|
|
256
|
-
progress.cancelling = false;
|
|
257
|
-
const tracker = createProgressTracker();
|
|
274
|
+
pluginArgs = prepared.args;
|
|
275
|
+
const tracker = beginProgress(prepared.target);
|
|
258
276
|
const feed = makeProgressFeeder(tracker);
|
|
259
277
|
return new Promise((resolvePromise) => {
|
|
260
278
|
const child = spawn(file, [...args, 'plugin', '--profile', profile, ...pluginArgs], {
|
|
@@ -319,6 +337,151 @@ export function runDshPlugin(profile, pluginArgs) {
|
|
|
319
337
|
});
|
|
320
338
|
});
|
|
321
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Adapt DSH Desktop's generation-scoped package manager to the existing
|
|
342
|
+
* market runner. There is no runtime import or dependency on Desktop: the
|
|
343
|
+
* Host supplies this public service only when the package is mounted there.
|
|
344
|
+
*/
|
|
345
|
+
export function createDesktopPluginRuntime(service, activeProfileDir, invokingDir = process.cwd(), timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
346
|
+
if (!isAbsolute(activeProfileDir) || activeProfileDir.includes('\0')) {
|
|
347
|
+
throw new Error('dsh-market: Desktop profile directory must be an absolute path without NUL');
|
|
348
|
+
}
|
|
349
|
+
if (!isAbsolute(invokingDir) || invokingDir.includes('\0')) {
|
|
350
|
+
throw new Error('dsh-market: Desktop invoking directory must be an absolute path without NUL');
|
|
351
|
+
}
|
|
352
|
+
const owner = Symbol('dsh-market desktop runtime');
|
|
353
|
+
let closed = false;
|
|
354
|
+
const runPlugin = async (_profile, pluginArgs) => {
|
|
355
|
+
if (closed) {
|
|
356
|
+
return {
|
|
357
|
+
exitCode: 127,
|
|
358
|
+
timedOut: false,
|
|
359
|
+
stdout: '',
|
|
360
|
+
stderr: 'dsh-market: Desktop package runtime is disposed',
|
|
361
|
+
cancelled: false,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const prepared = preparePluginArgs(activeProfileDir, pluginArgs);
|
|
365
|
+
if ('error' in prepared) {
|
|
366
|
+
logEvent('error', 'install', prepared.error);
|
|
367
|
+
return { exitCode: 1, timedOut: false, stdout: '', stderr: prepared.error, cancelled: false };
|
|
368
|
+
}
|
|
369
|
+
const abort = new AbortController();
|
|
370
|
+
let handle;
|
|
371
|
+
try {
|
|
372
|
+
handle = service.runPlugin(prepared.args, invokingDir, abort.signal);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
376
|
+
const busy = /another desktop pnpm operation is already running/i.test(message);
|
|
377
|
+
return {
|
|
378
|
+
exitCode: 127,
|
|
379
|
+
timedOut: false,
|
|
380
|
+
stdout: '',
|
|
381
|
+
stderr: message,
|
|
382
|
+
cancelled: false,
|
|
383
|
+
...(busy ? { busy: true } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const tracker = beginProgress(prepared.target);
|
|
387
|
+
const feed = makeProgressFeeder(tracker);
|
|
388
|
+
let stdout = '';
|
|
389
|
+
let stderr = '';
|
|
390
|
+
let timedOut = false;
|
|
391
|
+
const collectStdout = (chunk) => {
|
|
392
|
+
const text = chunk.toString();
|
|
393
|
+
stdout = (stdout + text).slice(-256 * 1024);
|
|
394
|
+
feed(text);
|
|
395
|
+
syncProgress(tracker);
|
|
396
|
+
};
|
|
397
|
+
const collectStderr = (chunk) => {
|
|
398
|
+
const text = chunk.toString();
|
|
399
|
+
stderr = (stderr + text).slice(-64 * 1024);
|
|
400
|
+
feed(text);
|
|
401
|
+
syncProgress(tracker);
|
|
402
|
+
};
|
|
403
|
+
handle.stdout.on('data', collectStdout);
|
|
404
|
+
handle.stderr.on('data', collectStderr);
|
|
405
|
+
let active;
|
|
406
|
+
let timer;
|
|
407
|
+
const done = (async () => {
|
|
408
|
+
try {
|
|
409
|
+
const outcome = await handle.done;
|
|
410
|
+
const failed = outcome.exitCode !== 0 || outcome.signal !== null || timedOut;
|
|
411
|
+
if (failed)
|
|
412
|
+
progress.error = tracker.snapshot.error;
|
|
413
|
+
const ignoredBuilds = tracker.snapshot.ignoredBuilds;
|
|
414
|
+
return {
|
|
415
|
+
exitCode: outcome.exitCode,
|
|
416
|
+
timedOut,
|
|
417
|
+
stdout,
|
|
418
|
+
stderr,
|
|
419
|
+
cancelled: active.userCancelled,
|
|
420
|
+
...(ignoredBuilds.length > 0 ? { ignoredBuilds } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
425
|
+
progress.error = tracker.snapshot.error;
|
|
426
|
+
return {
|
|
427
|
+
exitCode: 127,
|
|
428
|
+
timedOut,
|
|
429
|
+
stdout,
|
|
430
|
+
stderr: `${stderr}${stderr === '' ? '' : '\n'}${message}`,
|
|
431
|
+
cancelled: active.userCancelled,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
if (timer !== undefined)
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
progress.active = false;
|
|
438
|
+
progress.cancelling = false;
|
|
439
|
+
handle.stdout.off('data', collectStdout);
|
|
440
|
+
handle.stderr.off('data', collectStderr);
|
|
441
|
+
if (activeDesktopOperation === active)
|
|
442
|
+
activeDesktopOperation = null;
|
|
443
|
+
}
|
|
444
|
+
})();
|
|
445
|
+
active = { owner, cancel: () => { handle.cancel(); }, done, userCancelled: false };
|
|
446
|
+
activeDesktopOperation = active;
|
|
447
|
+
timer = setTimeout(() => {
|
|
448
|
+
timedOut = true;
|
|
449
|
+
abort.abort(new Error('dsh-market: Desktop package operation timed out'));
|
|
450
|
+
// The public handle owns an explicit process-tree cancellation path.
|
|
451
|
+
// Use it as well as AbortSignal so a structurally compatible provider
|
|
452
|
+
// that does not observe the signal cannot strand the route or teardown.
|
|
453
|
+
handle.cancel();
|
|
454
|
+
}, timeoutMs);
|
|
455
|
+
timer.unref?.();
|
|
456
|
+
return done;
|
|
457
|
+
};
|
|
458
|
+
const cancelOwned = (userCancelled) => {
|
|
459
|
+
const active = activeDesktopOperation;
|
|
460
|
+
if (active?.owner !== owner)
|
|
461
|
+
return false;
|
|
462
|
+
if (userCancelled)
|
|
463
|
+
active.userCancelled = true;
|
|
464
|
+
progress.cancelling = true;
|
|
465
|
+
active.cancel();
|
|
466
|
+
return true;
|
|
467
|
+
};
|
|
468
|
+
return {
|
|
469
|
+
runPlugin,
|
|
470
|
+
// The service is backed by Desktop's packaged pnpm; system discovery and
|
|
471
|
+
// global provisioning are neither needed nor allowed in this mode.
|
|
472
|
+
probePnpm: () => Promise.resolve(true),
|
|
473
|
+
provisionPnpm: () => Promise.resolve({ ok: true }),
|
|
474
|
+
cancelActive: () => cancelOwned(true),
|
|
475
|
+
dispose: async () => {
|
|
476
|
+
closed = true;
|
|
477
|
+
const active = activeDesktopOperation;
|
|
478
|
+
if (active?.owner !== owner)
|
|
479
|
+
return;
|
|
480
|
+
cancelOwned(false);
|
|
481
|
+
await active.done.catch(() => { });
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
322
485
|
/** Copy the tracker's snapshot into the singleton the status route reads. */
|
|
323
486
|
function syncProgress(tracker) {
|
|
324
487
|
const snap = tracker.snapshot;
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* dsh-market host entry: mounts the market's HTTP routes once the profile
|
|
3
3
|
* composes the webServer and shell services.
|
|
4
4
|
*/
|
|
5
|
+
import { createDesktopPluginRuntime } from './dsh-cli.js';
|
|
5
6
|
import { mountMarketRoutes } from './routes.js';
|
|
6
7
|
export const name = 'dsh-market';
|
|
7
8
|
/**
|
|
@@ -22,12 +23,41 @@ function argvProfile() {
|
|
|
22
23
|
return undefined;
|
|
23
24
|
}
|
|
24
25
|
export function apply(ctx, config) {
|
|
25
|
-
const resolved = {
|
|
26
|
-
profile: config?.profile ?? argvProfile() ?? 'web',
|
|
27
|
-
allowRestart: config?.allowRestart ?? true,
|
|
28
|
-
};
|
|
29
26
|
ctx.inject(['webServer', 'loader'], (hostCtx) => {
|
|
30
27
|
const host = hostCtx;
|
|
31
|
-
|
|
28
|
+
const desktopProfiles = ctx.get('desktopProfiles');
|
|
29
|
+
if (desktopProfiles === undefined) {
|
|
30
|
+
const resolved = {
|
|
31
|
+
profile: config?.profile ?? argvProfile() ?? 'web',
|
|
32
|
+
allowRestart: config?.allowRestart ?? true,
|
|
33
|
+
};
|
|
34
|
+
host.effect(() => mountMarketRoutes(host, resolved), 'dsh-market: http routes');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Desktop's supported cross-environment contract guarantees that
|
|
38
|
+
// desktopProfiles exists before Loader entries mount, and prescribes this
|
|
39
|
+
// presence check plus a nested desktopPnpm injection:
|
|
40
|
+
// https://github.com/anywhere-labs/deepseek-harness-desktop/blob/4f68147091e585aaa1d815f99d30a657b3842d7c/dsh-plugin-desktop/docs/plugin-services.md#L190-L243
|
|
41
|
+
// Ordinary DSH keeps the existing CLI path above.
|
|
42
|
+
hostCtx.inject(['desktopPnpm'], (desktopCtx) => {
|
|
43
|
+
const current = desktopProfiles.current;
|
|
44
|
+
const service = desktopCtx.desktopPnpm;
|
|
45
|
+
const runtime = createDesktopPluginRuntime(service, current.dir);
|
|
46
|
+
const resolved = {
|
|
47
|
+
profile: current.name,
|
|
48
|
+
profileDirectory: current.dir,
|
|
49
|
+
// Relaunching a raw Electron process would bypass Desktop's launcher
|
|
50
|
+
// lifecycle. The shell remains responsible for restart in this mode.
|
|
51
|
+
allowRestart: false,
|
|
52
|
+
};
|
|
53
|
+
const desktopHost = desktopCtx;
|
|
54
|
+
desktopHost.effect(() => {
|
|
55
|
+
const disposeRoutes = mountMarketRoutes(host, resolved, runtime);
|
|
56
|
+
return async () => {
|
|
57
|
+
disposeRoutes();
|
|
58
|
+
await runtime.dispose();
|
|
59
|
+
};
|
|
60
|
+
}, 'dsh-market: Desktop http routes and package operations');
|
|
61
|
+
});
|
|
32
62
|
});
|
|
33
63
|
}
|
package/lib/install.js
CHANGED
|
@@ -62,20 +62,21 @@ export async function withHoistRecovery(run, profile, pluginArgs) {
|
|
|
62
62
|
* pnpm's `#path:` selector (#18).
|
|
63
63
|
* @returns overall success (true when nothing needed retargeting).
|
|
64
64
|
*/
|
|
65
|
-
export async function retargetCollections(run, profile, before, target) {
|
|
65
|
+
export async function retargetCollections(run, profile, before, target, explicitDir) {
|
|
66
66
|
if (!target.startsWith('github:'))
|
|
67
67
|
return true;
|
|
68
|
-
const
|
|
68
|
+
const dir = profileDir(profile, explicitDir);
|
|
69
|
+
const junk = Object.keys(readInstalled(profile, dir)).filter((name) => {
|
|
69
70
|
if (before.has(name))
|
|
70
71
|
return false;
|
|
71
|
-
const root = join(
|
|
72
|
+
const root = join(dir, 'node_modules', name);
|
|
72
73
|
if (!existsSync(join(root, 'package.json')))
|
|
73
74
|
return true;
|
|
74
75
|
return !hasDshManifest(root);
|
|
75
76
|
});
|
|
76
77
|
let allOk = true;
|
|
77
78
|
for (const name of junk) {
|
|
78
|
-
const root = join(
|
|
79
|
+
const root = join(dir, 'node_modules', name);
|
|
79
80
|
const candidates = pluginSubdirs(root);
|
|
80
81
|
logEvent('info', 'install', `${name}: collection repo (root declares no dsh manifest); plugins inside: ${candidates.join(', ') || 'none'}`);
|
|
81
82
|
await run(profile, ['remove', name]);
|
|
@@ -100,13 +101,14 @@ export async function retargetCollections(run, profile, before, target) {
|
|
|
100
101
|
* next boot, so it is removed on the spot.
|
|
101
102
|
* @returns names kept and names removed as broken.
|
|
102
103
|
*/
|
|
103
|
-
export async function validateAddedPlugins(run, profile, before) {
|
|
104
|
-
const
|
|
104
|
+
export async function validateAddedPlugins(run, profile, before, explicitDir) {
|
|
105
|
+
const dir = profileDir(profile, explicitDir);
|
|
106
|
+
const addedNow = Object.keys(readInstalled(profile, dir)).filter(n => !before.has(n));
|
|
105
107
|
const keep = [];
|
|
106
108
|
const removedBroken = [];
|
|
107
109
|
for (const n of addedNow) {
|
|
108
|
-
const
|
|
109
|
-
if (hasDshManifest(
|
|
110
|
+
const packageDir = join(dir, 'node_modules', n);
|
|
111
|
+
if (hasDshManifest(packageDir) && entryArtifactExists(packageDir)) {
|
|
110
112
|
keep.push(n);
|
|
111
113
|
}
|
|
112
114
|
else {
|
package/lib/profile.js
CHANGED
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a profile name to its directory under DSH_HOME (default ~/.dsh).
|
|
11
|
+
* An explicit directory is used by hosts, such as DSH Desktop, that own the
|
|
12
|
+
* active profile location rather than deriving it from process environment.
|
|
13
|
+
*/
|
|
14
|
+
export function profileDir(profile, explicitDir) {
|
|
15
|
+
if (explicitDir !== undefined)
|
|
16
|
+
return explicitDir;
|
|
11
17
|
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
12
18
|
return join(home, 'profiles', profile);
|
|
13
19
|
}
|
|
@@ -24,9 +30,9 @@ const INBOX_BUNDLES = new Set([
|
|
|
24
30
|
'@deepseek-ai/dsh-headless',
|
|
25
31
|
]);
|
|
26
32
|
/** Community dependencies of the profile (in-box bundles filtered out). */
|
|
27
|
-
export function readInstalled(profile) {
|
|
33
|
+
export function readInstalled(profile, explicitDir) {
|
|
28
34
|
try {
|
|
29
|
-
const manifest = JSON.parse(readFileSync(join(profileDir(profile), 'package.json'), 'utf8'));
|
|
35
|
+
const manifest = JSON.parse(readFileSync(join(profileDir(profile, explicitDir), 'package.json'), 'utf8'));
|
|
30
36
|
const installed = {};
|
|
31
37
|
for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
|
|
32
38
|
if (!INBOX_BUNDLES.has(name))
|
|
@@ -43,9 +49,9 @@ export function readInstalled(profile) {
|
|
|
43
49
|
* readInstalled() filters out. This is the rollback snapshot (#65): restoring
|
|
44
50
|
* a filtered view would delete @deepseek-ai/dsh-base and friends.
|
|
45
51
|
*/
|
|
46
|
-
export function readManifestDeps(profile) {
|
|
52
|
+
export function readManifestDeps(profile, explicitDir) {
|
|
47
53
|
try {
|
|
48
|
-
const manifest = JSON.parse(readFileSync(join(profileDir(profile), 'package.json'), 'utf8'));
|
|
54
|
+
const manifest = JSON.parse(readFileSync(join(profileDir(profile, explicitDir), 'package.json'), 'utf8'));
|
|
49
55
|
return { ...manifest.dependencies };
|
|
50
56
|
}
|
|
51
57
|
catch {
|
|
@@ -63,8 +69,8 @@ export function readManifestDeps(profile) {
|
|
|
63
69
|
* manifest on the next run).
|
|
64
70
|
* @returns names whose entries were dropped or reverted, empty when nothing changed.
|
|
65
71
|
*/
|
|
66
|
-
export function restoreManifestDeps(profile, snapshot) {
|
|
67
|
-
const file = join(profileDir(profile), 'package.json');
|
|
72
|
+
export function restoreManifestDeps(profile, snapshot, explicitDir) {
|
|
73
|
+
const file = join(profileDir(profile, explicitDir), 'package.json');
|
|
68
74
|
let manifest;
|
|
69
75
|
try {
|
|
70
76
|
manifest = JSON.parse(readFileSync(file, 'utf8'));
|
|
@@ -87,9 +93,9 @@ export function restoreManifestDeps(profile, snapshot) {
|
|
|
87
93
|
return [...touched];
|
|
88
94
|
}
|
|
89
95
|
/** The version actually present in the profile's node_modules, or null. */
|
|
90
|
-
export function readInstalledVersion(profile, name) {
|
|
96
|
+
export function readInstalledVersion(profile, name, explicitDir) {
|
|
91
97
|
try {
|
|
92
|
-
const manifest = JSON.parse(readFileSync(join(profileDir(profile), 'node_modules', name, 'package.json'), 'utf8'));
|
|
98
|
+
const manifest = JSON.parse(readFileSync(join(profileDir(profile, explicitDir), 'node_modules', name, 'package.json'), 'utf8'));
|
|
93
99
|
return manifest.version ?? null;
|
|
94
100
|
}
|
|
95
101
|
catch {
|
|
@@ -97,10 +103,10 @@ export function readInstalledVersion(profile, name) {
|
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
/** Pinned commit per `owner/repo` from the profile lockfile's codeload tarball URLs. */
|
|
100
|
-
export function readLockCommits(profile) {
|
|
106
|
+
export function readLockCommits(profile, explicitDir) {
|
|
101
107
|
const commits = new Map();
|
|
102
108
|
try {
|
|
103
|
-
const lock = readFileSync(join(profileDir(profile), 'pnpm-lock.yaml'), 'utf8');
|
|
109
|
+
const lock = readFileSync(join(profileDir(profile, explicitDir), 'pnpm-lock.yaml'), 'utf8');
|
|
104
110
|
for (const m of lock.matchAll(/codeload\.github\.com\/([^/\s]+\/[^/\s]+)\/tar\.gz\/([0-9a-f]{40})/g)) {
|
|
105
111
|
commits.set(m[1].toLowerCase(), m[2]);
|
|
106
112
|
}
|
|
@@ -186,8 +192,8 @@ export function pluginSubdirs(root) {
|
|
|
186
192
|
* (#6 by @qichuang321.)
|
|
187
193
|
* @returns every package now allowed.
|
|
188
194
|
*/
|
|
189
|
-
export function setAllowBuilds(profile, packages) {
|
|
190
|
-
const file = join(profileDir(profile), 'pnpm-workspace.yaml');
|
|
195
|
+
export function setAllowBuilds(profile, packages, explicitDir) {
|
|
196
|
+
const file = join(profileDir(profile, explicitDir), 'pnpm-workspace.yaml');
|
|
191
197
|
let yaml = '';
|
|
192
198
|
try {
|
|
193
199
|
yaml = readFileSync(file, 'utf8');
|