@1e0zj/dsh-plugin-mall 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1e0zj/dsh-plugin-mall",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "dsh 插件市场:搜索 GitHub dsh-plugin 话题下的插件仓库,一键安装到本地 dsh profile(agent 工具 + 设置页插件市场 tab)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/client.js CHANGED
@@ -161,7 +161,7 @@ window.__ModuleLoader__.load({
161
161
  verifyBadge(props.verified),
162
162
  item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
163
163
  ),
164
- item.description ? h("div", { className: "mkt_desc" }, clip(item.description, 180)) : null,
164
+ item.description ? h("div", { className: "mkt_desc" }, item.description) : null,
165
165
  h("div", { className: "mkt_metaRow" },
166
166
  h("span", { className: "mkt_meta" }, "★" + item.stars),
167
167
  item.language ? h("span", { className: "mkt_meta" }, item.language) : null,
@@ -293,10 +293,17 @@ window.__ModuleLoader__.load({
293
293
  var _retryAt = useState(0);
294
294
  var retryAt = _retryAt[0];
295
295
  var setRetryAt = _retryAt[1];
296
+ var _restarting = useState(false);
297
+ var restarting = _restarting[0];
298
+ var setRestarting = _restarting[1];
296
299
 
297
300
  var call = useCallback(function (endpoint, payload) {
298
301
  return rpc.call("/market", endpoint, payload || {}).then(function (res) {
299
- if (!res || res.ok !== true) throw new Error((res && res.error) || "market request failed");
302
+ if (!res || res.ok !== true) {
303
+ var err = res && res.error;
304
+ var msg = err && typeof err === "object" ? (err.message || JSON.stringify(err)) : (err || "market request failed");
305
+ throw new Error(msg);
306
+ }
300
307
  return res.value;
301
308
  });
302
309
  }, [rpc]);
@@ -418,6 +425,30 @@ window.__ModuleLoader__.load({
418
425
  doInstallSpec("github:" + repo);
419
426
  }, [doInstallSpec]);
420
427
 
428
+ // 一键重启:loopback-only 的 /market restart 端点会 detached 拉起新的
429
+ // dsh 进程后退出当前进程;这里轮询 host 恢复后自动刷新页面。
430
+ var doRestart = useCallback(function () {
431
+ if (typeof window !== "undefined" && typeof window.confirm === "function") {
432
+ if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
433
+ }
434
+ setRestarting(true);
435
+ setError(null);
436
+ call("restart", {}).then(function () {
437
+ var tries = 0;
438
+ var ping = setInterval(function () {
439
+ tries++;
440
+ if (tries > 40) { clearInterval(ping); return; }
441
+ rpc.call("/market", "installed", {}).then(function () {
442
+ clearInterval(ping);
443
+ window.location.reload();
444
+ }).catch(function () { /* host 还在重启,继续等 */ });
445
+ }, 3000);
446
+ }).catch(function (e) {
447
+ setRestarting(false);
448
+ setError(errorText(e));
449
+ });
450
+ }, [call, rpc]);
451
+
421
452
  var doUninstall = useCallback(function (name) {
422
453
  setRemoving(function (prev) {
423
454
  var next = Object.assign({}, prev, { [name]: true });
@@ -478,6 +509,7 @@ window.__ModuleLoader__.load({
478
509
  ),
479
510
  h("button", { className: "mkt_btn mkt_btnPrimary", disabled: loading, onClick: doSearch }, loading ? "搜索中…" : "搜索"),
480
511
  h("button", { className: "mkt_btn", onClick: refreshInstalled }, "刷新已装"),
512
+ h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: doRestart }, restarting ? "重启中…" : "重启 dsh"),
481
513
  h("label", { className: "mkt_check" },
482
514
  h("input", { type: "checkbox", checked: verifiedOnly, onChange: function (e) { setVerifiedOnly(e.target.checked); } }),
483
515
  "只看已验证插件")
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ import z from "@deepseek-ai/schemastery";
17
17
  import { defineTool } from "@deepseek-ai/dsh-tools";
18
18
  import { existsSync, readFileSync } from "node:fs";
19
19
  import { join } from "node:path";
20
+ import { spawn } from "node:child_process";
20
21
  import { resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
21
22
  import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions } from "./github.js";
22
23
  import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker } from "./installer.js";
@@ -28,6 +29,7 @@ export const Config = z.object({
28
29
  defaultProfile: z.string().default("web"),
29
30
  apiBase: z.string().default("https://api.github.com"),
30
31
  perPageMax: z.number().default(30),
32
+ allowRestart: z.boolean().default(true),
31
33
  });
32
34
 
33
35
  /** Clip long strings for compact model-facing output. */
@@ -128,7 +130,10 @@ function rpcOk(value) {
128
130
  }
129
131
 
130
132
  function rpcFail(error) {
131
- return { ok: false, error: error?.message ?? String(error) };
133
+ // dsh connection RPC 的响应信封校验(dsh-client-connection rpcResultSchema)
134
+ // 要求 error 为 discriminated object:{code, message, details}。code 取
135
+ // 通用 "internal",否则整条错误会被 zod 以 invalid_union 吞掉。
136
+ return { ok: false, error: { code: "internal", message: error?.message ?? String(error), details: {} } };
132
137
  }
133
138
 
134
139
  /**
@@ -145,7 +150,7 @@ function rpcFail(error) {
145
150
  * @returns the {ok, value|error} envelope.
146
151
  */
147
152
  async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
148
- const { defaultProfile = "web", apiBase = "https://api.github.com", perPageMax = 30 } = config;
153
+ const { defaultProfile = "web", apiBase = "https://api.github.com", perPageMax = 30, allowRestart = true } = config;
149
154
  switch (endpoint) {
150
155
  case "search": {
151
156
  const perPage = Math.min(Math.max(Math.trunc(payload?.perPage ?? 10) || 10, 1), Math.trunc(perPageMax) || 30);
@@ -246,6 +251,25 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
246
251
  return rpcFail(error);
247
252
  }
248
253
  }
254
+ case "restart": {
255
+ // 一键重启:detached 拉起新 dsh 进程(用当前进程的 argv 重建启动命令)
256
+ // 后退出自己。仅 loopback 直连可调(channel 级 authority 已限制);
257
+ // allowRestart:false 时禁用(进程由 systemd/pm2 等托管时接管重启)。
258
+ if (allowRestart !== true) return rpcFail(new Error("restart disabled by config (allowRestart: false)"));
259
+ const script = process.argv[1];
260
+ const scriptArgs = process.argv.slice(2);
261
+ if (typeof script !== "string" || script.length === 0 || !existsSync(script)) {
262
+ return rpcFail(new Error("cannot determine the dsh launch command for an automatic restart — please restart manually"));
263
+ }
264
+ const relaunch = `"${process.execPath}" "${script}"${scriptArgs.length > 0 ? ` ${scriptArgs.map((arg) => `"${arg}"`).join(" ")}` : ""}`;
265
+ const launcher = process.platform === "win32"
266
+ ? `timeout /t 2 /nobreak >nul & ${relaunch}`
267
+ : `sleep 2 && ${relaunch}`;
268
+ const child = spawn(launcher, { shell: true, detached: true, stdio: "ignore", cwd: process.cwd(), windowsHide: true });
269
+ child.unref();
270
+ setTimeout(() => process.exit(0), 800);
271
+ return rpcOk({ restarting: true });
272
+ }
249
273
  case "jobCancel": {
250
274
  try {
251
275
  return rpcOk({ result: tracker.cancel(payload?.jobId) });
package/src/installer.js CHANGED
@@ -361,6 +361,35 @@ export function createJobTracker() {
361
361
  };
362
362
  }
363
363
 
364
+ // ── pnpm self-heal (corepack) ───────────────────────────────────────────────
365
+
366
+ /**
367
+ * Try to provision pnpm once via `corepack enable pnpm` (corepack ships with
368
+ * Node). Output lands in the caller's job log; returns whether a retry of the
369
+ * pnpm spawn is worth attempting.
370
+ */
371
+ async function enablePnpmViaCorepack(push) {
372
+ push("\n[dsh-plugin-mall] pnpm not found on PATH — trying `corepack enable pnpm` once\n");
373
+ return await new Promise((resolve) => {
374
+ let proc;
375
+ try {
376
+ proc = spawn("corepack", ["enable", "pnpm"], {
377
+ env: process.env,
378
+ shell: process.platform === "win32",
379
+ stdio: ["ignore", "pipe", "pipe"],
380
+ windowsHide: true,
381
+ });
382
+ } catch {
383
+ resolve(false);
384
+ return;
385
+ }
386
+ proc.on("error", () => resolve(false));
387
+ proc.stdout?.on("data", (data) => push(data.toString()));
388
+ proc.stderr?.on("data", (data) => push(data.toString()));
389
+ proc.on("close", (code) => resolve(code === 0));
390
+ });
391
+ }
392
+
364
393
  // ── the background install job ──────────────────────────────────────────────
365
394
 
366
395
  /**
@@ -381,6 +410,7 @@ export function runInstall({ profile, spec }) {
381
410
  deltaQueue.push(text);
382
411
  };
383
412
  let current = undefined;
413
+ let pnpmSelfHealed = false;
384
414
 
385
415
  const spawnAdd = () => {
386
416
  const proc = spawn("pnpm", ["add", spec, "--reporter=append-only"], {
@@ -427,6 +457,17 @@ export function runInstall({ profile, spec }) {
427
457
 
428
458
  const settle = async (outcome) => {
429
459
  if (outcome.spawnError !== undefined) {
460
+ // pnpm 缺失时先尝试 corepack 自愈一次,成功则重跑安装。
461
+ if (outcome.spawnError.code === "ENOENT" && !pnpmSelfHealed) {
462
+ pnpmSelfHealed = true;
463
+ const healed = await enablePnpmViaCorepack(push);
464
+ if (healed) {
465
+ const retry = spawnAdd();
466
+ current = retry.proc;
467
+ return settle(await retry.done);
468
+ }
469
+ return { status: "failed", detail: "pnpm not found on PATH and `corepack enable pnpm` could not provision it — install pnpm (e.g. `npm i -g pnpm`) to manage profile plugins" };
470
+ }
430
471
  const hint = outcome.spawnError.code === "ENOENT"
431
472
  ? "pnpm not found on PATH — install pnpm (e.g. `corepack enable pnpm`) to manage profile plugins"
432
473
  : `could not start pnpm: ${outcome.spawnError.message}`;
@@ -490,7 +531,7 @@ function failedNow(detail) {
490
531
  * `dsh.profile.bundles` (the removed dependency's bundle entry drops out) and
491
532
  * deletes the client loader row `ensureClientRow` had registered for it.
492
533
  */
493
- export function runRemove({ profile, packageName }) {
534
+ export function runRemove({ profile, packageName }, selfHealed = false) {
494
535
  let profileDir;
495
536
  try {
496
537
  profileDir = resolveProfileDir(profile);
@@ -522,8 +563,13 @@ export function runRemove({ profile, packageName }) {
522
563
  const done = new Promise((resolve) => {
523
564
  proc.on("error", (error) => resolve({ spawnError: error }));
524
565
  proc.on("close", (exitCode) => resolve({ exitCode, signal: proc.signalCode }));
525
- }).then((outcome) => {
566
+ }).then(async (outcome) => {
526
567
  if (outcome.spawnError !== undefined) {
568
+ // pnpm 缺失时先 corepack 自愈一次再重试(重试在新 producer 里跑)。
569
+ if (outcome.spawnError.code === "ENOENT" && selfHealed !== true) {
570
+ const healed = await enablePnpmViaCorepack(() => {});
571
+ if (healed) return runRemove({ profile, packageName }, true);
572
+ }
527
573
  const hint = outcome.spawnError.code === "ENOENT"
528
574
  ? "pnpm not found on PATH — install pnpm (e.g. `corepack enable pnpm`) to manage profile plugins"
529
575
  : `could not start pnpm: ${outcome.spawnError.message}`;