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 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 installing Node.js or touching the command line. Plugin marketplace, mobile remote control, and IM Channels are on its roadmap.
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 生态打造的现代化桌面端,让用户无需配置 Node.js 或执行命令,即可启动和管理本地 Harness 服务。项目后续还将支持插件市场、移动端远程控制和 IM Channels。
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
- "star": "eGUBIq_star",
446
- "barWave": "eGUBIq_barWave",
447
- "filterBtnOn": "eGUBIq_filterBtnOn",
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
- "cmd": "eGUBIq_cmd",
450
- "sp": "eGUBIq_sp",
451
- "descTight": "eGUBIq_descTight",
452
- "actWarn": "eGUBIq_actWarn",
453
- "staleAction": "eGUBIq_staleAction",
454
- "pageBtn": "eGUBIq_pageBtn",
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
- "actLive": "eGUBIq_actLive",
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
- "okState": "eGUBIq_okState",
485
+ "card": "eGUBIq_card",
486
+ "star": "eGUBIq_star",
480
487
  "dangerBtn": "eGUBIq_dangerBtn",
481
- "catsToggle": "eGUBIq_catsToggle",
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
- "act": "eGUBIq_act",
522
- "err": "eGUBIq_err",
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
- "tag": "eGUBIq_tag"
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
- pluginArgs = pluginArgsFor(profileDir(profile), pluginArgs);
235
- const target = pluginArgs[pluginArgs.length - 1] ?? '';
236
- if (!TARGET_RE.test(target)) {
237
- logEvent('error', 'install', `unsafe plugin target rejected: ${JSON.stringify(target)}`);
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
- // pnpm's structured reporter must be appended after the target is extracted
241
- // (the allowlist above validates the last argument).
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
- host.effect(() => mountMarketRoutes(host, resolved), 'dsh-market: http routes');
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 junk = Object.keys(readInstalled(profile)).filter((name) => {
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(profileDir(profile), 'node_modules', name);
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(profileDir(profile), 'node_modules', name);
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 addedNow = Object.keys(readInstalled(profile)).filter(n => !before.has(n));
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 dir = join(profileDir(profile), 'node_modules', n);
109
- if (hasDshManifest(dir) && entryArtifactExists(dir)) {
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
- /** Resolve a profile name to its directory under DSH_HOME (default ~/.dsh). */
10
- export function profileDir(profile) {
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');