dsh-superagent-console 0.1.2 → 0.1.3

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
@@ -1,78 +1,77 @@
1
1
  # dsh-superagent-console
2
2
 
3
- 桌面端 Superagent 控制台插件:侧边栏面板 + 同源代理 + **引擎检测与在线安装** + **模型供应商同步**。引擎仍运行在独立的 superagent profile(tmux,默认 127.0.0.1:3082),本插件不承载任何引擎状态。
3
+ dsh-superagent 引擎的桌面控制台插件:同源代理 + 引擎状态检测 + 引导式在线安装 + 插件管理 + 模型供应商同步。引擎运行于独立的 superagent profile(默认 `127.0.0.1:3082`),本插件不承载任何引擎状态。
4
+
5
+ ## 功能
6
+
7
+ - **同源代理**:宿主 `/orchestrator/api` 转发至引擎(补 `x-orchestrator-request` 头),控制台自有 API 位于 `/superagent-console/api`
8
+ - **引擎检测**:L0–L3 分层探针,状态与动作一一映射
9
+ - **引导式在线安装**:八段流水线(`prereqs → checkout → build → profile → plugin → patch → start → verify`),每段幂等、支持断点续跑
10
+ - **插件管理**:面板内直接安装/移除/启停引擎 profile 的插件
11
+ - **模型供应商同步**:把宿主生效的 `llm-pi-ai.providers` 同步到引擎 profile
4
12
 
5
13
  ## 架构
6
14
 
7
15
  ```
8
16
  桌面 App(desktop profile)
9
17
  └─ dsh-superagent-console
10
- ├─ 宿主 /orchestrator/api → 同源代理转发 127.0.0.1:3082(补 x-orchestrator-request 头)
18
+ ├─ 宿主 /orchestrator/api → 同源代理转发引擎(补 x-orchestrator-request 头)
11
19
  ├─ 宿主 /superagent-console/api → 控制台自有 API(检测 / 安装 / 启动 / 重启 / 插件管理 / 供应商同步)
12
20
  └─ 客户端 → 门控面板:引擎在线时挂官方面板(含「插件」管理 Tab),
13
- 否则渲染安装引导卡(状态卡)
14
-
15
- ## 插件管理 Tab(2026-11-04)
16
-
17
- 官方面板内新增「插件」Tab(位于「设置」之后),管理 superagent 引擎 profile 的插件依赖:
18
-
19
- - **列表**:profile package.json 的依赖清单 + 已安装版本/描述 + 是否声明 `dsh.bundle` + 是否在 `dsh.profile.bundles` 挂载集内;引擎核心 `dsh-superagent` 标记保护(不可移除/停用)。
20
- - **安装 / 移除**:走官方 CLI 路径 `dsh plugin --profile superagent add|remove <spec>`(execFile argv 数组、单飞互斥、输出脱敏、audit.jsonl 审计),复用安装器的 bundle reconcile 语义;改动在**重启引擎后生效**,页面提供重启按钮。
21
- - **启停(挂载集编辑)**:直接原子改写 profile package.json 的 `dsh.profile.bundles`(保留 0600 权限;无变更不写盘);与安装/移除共用同一互斥。
22
- - API:`GET/POST /superagent-console/api/plugins[/install|/remove|/enable|/operation]`,实现见 `src/plugins.ts`(运行时验证:假 profile 16 项断言 + 真实 profile 只读 list)。
23
-
24
- vendor 补丁(seams):`sync-client.mjs` 在每次 vendor 复制后,对 `client/route.ts`(TABS/Route/parseRoute)、`client/app.tsx`(路由分支 + import)、`client/locales.ts`(`tab.plugins` + `plugins.*` 双语词典)施加**精确锚点补丁**;上游锚点漂移会让 sync 显式失败,不会打出半接线的 Tab。页面本体在 `src/client/plugins-page.tsx`(自有文件,不受 vendor 覆盖)。
21
+ 否则渲染安装引导状态卡
25
22
  ```
26
23
 
27
- 客户端构建:`sync-client.mjs` 从原型 checkout **递归 vendor 客户端源码图**到 `src/vendor/`(104 个文件,树形镜像,相对引用原样保留),随后施加「插件 Tab」seam 补丁(见下节),再由 tsdown 打成带 `window.__ModuleLoader__` banner 的自建 bundle(id `dsh-superagent-console`)。运行时外部依赖只有 react 系,与官方编译产物一致(零 node:*、零 zod 内联)。
28
-
29
- ## 引擎检测(L0-L3 分层探针)
24
+ ## 引擎检测(L0–L3 分层探针)
30
25
 
31
26
  | 层 | 探测 | 判定 |
32
27
  |---|---|---|
33
28
  | L0 | 服务端 fetch `{target}/orchestrator/api/state`(带 `x-orchestrator-request: 1`) | 引擎可达 |
34
- | L1 | `~/.dsh/profiles/<profile>/package.json` 依赖含 `dsh-superagent` + checkout 有 `lib/` 产物 | 服务已部署 |
35
- | L2 | `scripts/dsh-superagent status`(exit 0 健康 / 1 停止 / 2 组件错误 / 3 等待 setup) | 健康细分 |
29
+ | L1 | 引擎 profile 的 package.json 依赖含 `dsh-superagent` 且 checkout 存在 `lib/` 产物 | 服务已部署 |
30
+ | L2 | 引擎生命周期脚本 `scripts/dsh-superagent status`(exit 0 健康 / 1 停止 / 2 组件错误 / 3 等待 setup) | 健康细分 |
36
31
  | L3 | dsh ≥0.2.1、node ^22.19\|\|≥24、pnpm、git、tmux、python3(缺项附补齐提示命令) | 前置依赖 |
37
32
 
38
- 状态 → 动作一一映射:`running` → 官方面板;`installed-stopped` → 启动按钮;`installed-unhealthy` → 重启按钮;`not-installed` → 在线安装;`prereq-missing` → 列出缺项与补齐命令(不提供安装按钮)。
33
+ 状态 → 动作映射:`running` → 官方面板;`installed-stopped` → 启动按钮;`installed-unhealthy` → 重启按钮;`not-installed` → 在线安装;`prereq-missing` → 列出缺项与补齐命令(不提供安装按钮)。
39
34
 
40
35
  探测 PATH 由 `spawnPath` 组装:运行中 node 的 bin 目录 + 配置的 `extraPath` + **桌面 App 自带的 dsh CLI 目录**(从运行中二进制相对推导 `<App>.app/Contents/Resources/runtime/cli/bin`,存在才追加)。因此桌面用户无需自行安装 dsh;`extraPath` 中显式安装的用户版本优先于内置 CLI。
41
36
 
42
- ## 在线安装流水线(8 段)
37
+ ## 在线安装流水线
43
38
 
44
39
  `prereqs → checkout → build → profile → plugin → patch → start → verify`
45
40
 
46
- - **来源**:`git clone` 官方仓库(npm 上无此包——`dsh-superagent` 404 且 `private: true`,GitHub 无 release;源码构建 + link 是官方唯一路径)。
41
+ - **来源**:`git clone` 官方仓库(`dsh-superagent` 在 npm 上不存在且 `private: true`,GitHub 无 release;源码构建 + link 是官方唯一路径)。
47
42
  - **自管目录**:默认 `~/.dsh/state/superagent-console/engine`(clone 与构建位置),随附 `setup.json`(断点续跑状态)与 `audit.jsonl`(审计日志)。
48
43
  - **守卫**:
49
- - `dsh <profile> --from-default-profile web` 是**覆盖式**命令(源码确认:replace a fresh copy),profile 目录存在即拒绝执行;
50
- - profile 的 `cordis.patch.yml` 已存在则不覆盖(内有操作者手工维护的 Source 配置);
44
+ - `dsh <profile> --from-default-profile web` 是覆盖式命令(源码确认:replace a fresh copy),profile 目录存在即拒绝执行;
45
+ - profile 的 `cordis.patch.yml` 已存在则不覆盖(其中含操作者手工维护的 Source 配置);
51
46
  - clone 目标目录非空且非 git 仓库即拒绝;
52
47
  - 单实例锁(进程内互斥 + pid 锁文件,跨桌面重启可检测陈锁);
53
- - 全部命令走 `execFile` argv 数组(永不 shell 拼接),超时必杀,审计脱敏 `user:pass@`。
54
- - **每段幂等**:已完成段自动跳过,失败段重按"继续安装"即从断点续跑。
48
+ - 全部命令走 `execFile` argv 数组(不经过 shell 拼接),超时必杀,审计脱敏 `user:pass@`。
49
+ - **每段幂等**:已完成段自动跳过,失败段重按「继续安装」即从断点续跑。
55
50
  - 首次启动 exit 3(等待 Source setup)属预期,verify 段放行。
56
51
 
57
- ## 启动 / 重启引擎
52
+ 启动/重启复用引擎自带生命周期脚本 `scripts/dsh-superagent start|restart`(无头托管、拒绝重复启动、URL 冲突检测),`--profile/--url` 由插件配置注入。
58
53
 
59
- 复用引擎自带生命周期脚本 `scripts/dsh-superagent start|restart`(tmux 无头托管、拒绝重复启动、尊重 URL 冲突检测),`--profile/--url` 由插件配置注入。
54
+ ## 插件管理 Tab
55
+
56
+ 官方面板内新增「插件」Tab,管理 superagent 引擎 profile 的插件依赖:
57
+
58
+ - **列表**:profile package.json 依赖清单 + 已安装版本/描述 + 是否声明 `dsh.bundle` + 是否在 `dsh.profile.bundles` 挂载集内;引擎核心 `dsh-superagent` 标记保护(不可移除/停用)。
59
+ - **安装 / 移除**:走官方 CLI 路径 `dsh plugin --profile superagent add|remove <spec>`(单飞互斥、输出脱敏、审计日志),复用安装器的 bundle reconcile 语义;改动在**重启引擎后生效**,页面提供重启按钮。
60
+ - **启停(挂载集编辑)**:原子改写 profile package.json 的 `dsh.profile.bundles`(保留 0600 权限;无变更不写盘);与安装/移除共用同一互斥。
60
61
 
61
62
  ## 模型供应商同步
62
63
 
63
- 引擎的模型目录来自其所在 profile 的 `llm-pi-ai` 配置,且引擎管理 API 对供应商**只读**(settings 块仅 policy/eval/executor/pause/source),因此引擎侧唯一写入面是 `~/.dsh/profiles/<profileName>/cordis.patch.yml` 的 `llm-pi-ai` 条目。本插件把安装宿主(desktop/web profile)当前生效的 `llm-pi-ai.providers`(settings 服务 `describe()` 读取,含 dsh-sub2api-sync 写入的路由)同步过去:
64
+ 引擎的模型目录来自其所在 profile 的 `llm-pi-ai` 配置,且引擎管理 API 对供应商只读,因此引擎侧唯一写入面是 profile `cordis.patch.yml` 的 `llm-pi-ai` 条目。本插件把安装宿主当前生效的 `llm-pi-ai.providers`(settings 服务 `describe()` 读取)同步过去:
64
65
 
65
66
  - **merge(默认)**:只把引擎缺少的 provider 追加进 `providers:`,已有条目一律不动(尊重手工维护);
66
67
  - **mirror**:引擎 providers 全量替换为本端(减去排除清单),引擎侧多出的删除;
67
68
  - **排除清单** `syncExclude`:其中的 provider id 既不复制也不删除;
68
69
  - **凭据守卫**:provider 配置携带明文 `apiKey`/`apiToken` 字段时跳过并告警——只同步引用型凭据(`apiKeyEnv` 环境变量名),秘密值永不复制;
69
- - **文件手术**:只改 `llm-pi-ai` 这一个条目,其余条目(Source 配置、`agent-default-model` 等)与全部注释逐字节保留;写前时间戳备份(保留最近 5 份)、临时文件原子写、写后 YAML 重解析验证(`llm-pi-ai` 条目不完整即拒绝落盘);
70
+ - **文件手术**:只改 `llm-pi-ai` 这一个条目,其余条目与全部注释逐字节保留;写前时间戳备份(保留最近 5 份)、临时文件原子写、写后 YAML 重解析验证(`llm-pi-ai` 条目不完整即拒绝落盘);
70
71
  - **幂等**:输入不变时第二跑零写入;
71
72
  - **生效**:写盘后需重启引擎(卡片提示一键重启);`autoSync` 开启时监听宿主 settings 变化(30s 轮询 diff),`autoRestart` 开启且引擎在运行时同步后自动重启。
72
73
 
73
- API:`GET /superagent-console/api/providers`(两侧摘要 + diff + 同步状态)、`POST /superagent-console/api/providers/sync`(body `{ mode? }`,409 单飞冲突)。settings 服务经 `ctx.inject(['settings'], …)` 可选注入:宿主没有该服务时同步功能不激活,插件其余功能不受影响。
74
-
75
- ## 配置项(默认值均为通用约定,不绑定具体机器)
74
+ ## 配置项
76
75
 
77
76
  | 键 | 默认 | 说明 |
78
77
  |---|---|---|
@@ -86,40 +85,18 @@ API:`GET /superagent-console/api/providers`(两侧摘要 + diff + 同步状
86
85
  | `autoSync` | `false` | 宿主 settings 变化时自动同步引擎 profile(30s 轮询 diff) |
87
86
  | `autoRestart` | `false` | 自动同步后且引擎在运行时自动重启引擎 |
88
87
 
89
- ## 构建
90
-
91
- ```sh
92
- pnpm install
93
- pnpm build # 先 sync-client(vendor 源码图),再 tsdown(node ESM + browser CJS 双产物)
94
- pnpm check # typecheck + build
95
- ```
96
-
97
- 升级引擎面板:原型 checkout 更新后 `pnpm build && cd ../dsh-superagent-console && pnpm build`(vendor 重新拉取源码图)。若在 worktree 内构建,sync 脚本会经 `git rev-parse --git-common-dir` 定位主 checkout 的兄弟目录。
98
-
99
- ## 代理独立验证(不碰桌面进程)
88
+ ## 安装
100
89
 
101
90
  ```sh
102
- pnpm test:proxy # 需要引擎已启动;在 3088 起临时服务器实测转发
103
- ```
104
-
105
- ## 安装隔离演练(不碰现有 superagent profile)
106
-
107
- ```sh
108
- pnpm test:install -- --status-only # 仅跑检测探针
109
- pnpm test:install # 全流程:superagent-test profile + /tmp checkout + 3099 端口
110
- ```
91
+ # 从 npm 安装
92
+ dsh plugin --profile desktop add dsh-superagent-console
111
93
 
112
- 演练产物清理(验完执行,注意这些路径仅属于演练):
113
-
114
- ```sh
115
- python3 /tmp/sa-console-rehearsal/engine/scripts/dsh-superagent stop --profile superagent-test --url http://127.0.0.1:3099
116
- rm -rf /tmp/sa-console-rehearsal ~/.dsh/profiles/superagent-test ~/.dsh/state/superagent-test
94
+ # 或从本地源码安装(开发场景)
95
+ dsh plugin --profile desktop add "link:$PWD"
117
96
  ```
118
97
 
119
- ## 安装到桌面端
98
+ 安装后桌面 App 需重载 profile 才会加载插件与客户端。
120
99
 
121
- ```sh
122
- dsh plugin --profile desktop add "link:$PWD"
123
- ```
100
+ ## License
124
101
 
125
- 安装后桌面 App 需重载 profile 才会加载插件与客户端。重建后重新激活即生效,勿重装。
102
+ MIT
package/lib/client.js CHANGED
@@ -4,8 +4,31 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ // \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
7
28
  let react = require("react");
8
29
  let react_dom = require("react-dom");
30
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
9
32
  let react_jsx_runtime = require("react/jsx-runtime");
10
33
  // src/vendor/navigation.ts
11
34
  /**
@@ -1334,13 +1357,14 @@ window.__ModuleLoader__.load({
1334
1357
  });
1335
1358
  }
1336
1359
  // src/client/plugins-page.tsx
1337
- /**
1338
- * Plugins tab for the official panel, styled after the DSH native plugin
1339
- * manager: a header with refresh and add-plugin actions, an install dialog
1340
- * with guide and examples, classified install failures, an uninstall
1341
- * confirmation, and a restart notice. Backed by the console API
1342
- * (/superagent-console/api/plugins*) and rendered with the panel's own
1343
- * primitives (dt-* classes mirror DSH ui-primitives).
1360
+ /**
1361
+ * Plugins tab for the official panel. The install dialog is rebuilt on the
1362
+ * desktop client's own ui-primitives (required at runtime — kept external by
1363
+ * tsdown, resolved by the ModuleLoader registry exactly like the native
1364
+ * manager does), so the modal shares the native plugin dialog's typography,
1365
+ * colors, focus rings and animation. Page-level dialog rules are ported
1366
+ * verbatim from the native manager's CSS module under sgc-* class names
1367
+ * (widths, gaps, the safety footer, the anchored registry popover).
1344
1368
  *
1345
1369
  * Backend contract (src/plugins.ts): install/remove run
1346
1370
  * dsh plugin --profile <name> add|remove server-side (single-flight,
@@ -1375,26 +1399,75 @@ window.__ModuleLoader__.load({
1375
1399
  if (/\bENOTFOUND\b|\bECONNRESET\b|\bETIMEDOUT\b|\bECONNREFUSED\b|\bEAI_AGAIN\b|FETCH_ERROR|Could not resolve/i.test(log)) return "network";
1376
1400
  return "generic";
1377
1401
  }
1378
- const GUIDE_EXAMPLES = [
1402
+ const GUIDE_ITEMS = [
1379
1403
  {
1380
1404
  key: "Npm",
1381
- example: "dsh-plugin-whale-pet"
1405
+ example: "dsh-plugin-whale-pet",
1406
+ titleKey: "plugins.installGuideNpm",
1407
+ hintKey: "plugins.installGuideNpmHint"
1382
1408
  },
1383
1409
  {
1384
1410
  key: "Git",
1385
- example: "github:owner/dsh-plugin-example"
1411
+ example: "github:owner/dsh-plugin-example",
1412
+ titleKey: "plugins.installGuideGit",
1413
+ hintKey: "plugins.installGuideGitHint"
1386
1414
  },
1387
1415
  {
1388
1416
  key: "Path",
1389
- example: "/Users/me/MyCode/dsh-plugin-example"
1417
+ example: "/Users/me/MyCode/dsh-plugin-example",
1418
+ titleKey: "plugins.installGuidePath",
1419
+ hintKey: "plugins.installGuidePathHint"
1390
1420
  }
1391
1421
  ];
1392
- function GuideText({ kind }) {
1393
- const { t } = usePanel();
1394
- if (kind === "Npm") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("plugins.installGuideNpm") });
1395
- if (kind === "Git") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("plugins.installGuideGit") });
1396
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("plugins.installGuidePath") });
1397
- }
1422
+ /**
1423
+ * Dialog rules ported from the native manager's PluginManagerPage CSS module
1424
+ * (values verbatim; class names renamed to the sgc- prefix so they cannot
1425
+ * collide with either the native or the engine-panel classes).
1426
+ */
1427
+ const SGC_CSS = `
1428
+ .sgc-installDialog{width:min(560px,100%);max-height:min(800px,100%)}
1429
+ .sgc-installContent{min-height:0;overflow-y:auto}
1430
+ .sgc-installBody{flex-direction:column;gap:12px;min-width:0;display:flex}
1431
+ .sgc-installField{flex-direction:column;gap:6px;font-size:13px;display:flex}
1432
+ .sgc-installField input[type=text]{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);height:40px;font:inherit;color:var(--dsw-alias-label-primary);outline:none;padding:0 14px;font-size:13px}
1433
+ .sgc-installField input[type=text]:focus,.sgc-registryCustomField:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:inset 0 0 0 .5px var(--dsw-alias-state-business-primary)}
1434
+ .sgc-optionsRow{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 12px;display:flex}
1435
+ .sgc-guideToggle{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;align-self:flex-start;align-items:center;gap:4px;margin-top:4px;padding:0;font-size:12.5px;display:inline-flex}
1436
+ .sgc-guideToggle:hover{color:var(--dsw-alias-label-primary)}
1437
+ .sgc-guideChevron{transition:transform .16s}
1438
+ .sgc-guideToggle[aria-expanded=true] .sgc-guideChevron,.sgc-registryToggle[aria-expanded=true] .sgc-guideChevron{transform:rotate(180deg)}
1439
+ .sgc-guide{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1);flex-direction:column;gap:10px;padding:8px 14px;display:flex}
1440
+ .sgc-guideList{flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;display:flex}
1441
+ .sgc-guideItem{align-items:flex-start;gap:10px;padding:8px 0;display:flex}
1442
+ .sgc-guideItem>button{align-self:center}
1443
+ .sgc-guideMain{flex-direction:column;flex:1;gap:0;min-width:0;display:flex}
1444
+ .sgc-guideTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}
1445
+ .sgc-guideHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px;line-height:18px}
1446
+ .sgc-guideExample{color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere;font-size:12px;line-height:20px}
1447
+ .sgc-guideExample code{font-family:var(--ds-font-family-code)}
1448
+ .sgc-guideExampleLabel{color:var(--dsw-alias-label-tertiary)}
1449
+ .sgc-registryToggle{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2);height:28px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:6px;padding:0 8px 0 10px;font-size:12.5px;display:inline-flex}
1450
+ .sgc-registryToggle:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
1451
+ .sgc-registryToggle:disabled{cursor:default;opacity:.6}
1452
+ .sgc-registryToggle[aria-expanded=true]{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-3)}
1453
+ .sgc-registryChosen{color:var(--dsw-alias-label-primary);font-weight:500}
1454
+ .sgc-registry{z-index:1100;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border-radius:var(--dsw-radius-lg);width:min(440px,calc(100vw - 24px));box-shadow:var(--dsw-elevation-prominent);border:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:4px;min-width:0;margin:0;padding:8px;display:flex;position:fixed}
1455
+ .sgc-registryOption{border-radius:var(--dsw-radius-md);cursor:pointer;border:0;flex-direction:column;gap:4px;padding:8px 10px;display:flex}
1456
+ .sgc-registryOption:hover{background:var(--dsw-alias-interactive-bg-hover)}
1457
+ label.sgc-registryOption{flex-direction:row;align-items:flex-start;gap:10px}
1458
+ .sgc-registryOption input[type=radio]{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);flex:none;margin:2px 0 0}
1459
+ .sgc-registryTitle{color:var(--dsw-alias-label-primary);flex-wrap:wrap;align-items:center;gap:8px;font-size:13px;line-height:20px;display:flex}
1460
+ .sgc-registryHint{color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere;font-size:12px;line-height:18px}
1461
+ .sgc-registryCustomField{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);outline:none;margin-left:26px;padding:0 10px;font-size:13px}
1462
+ .sgc-registryCustomError{margin:0 0 0 26px}
1463
+ .sgc-installFooter{flex-direction:column;flex:1;gap:20px;min-width:0;display:flex}
1464
+ .sgc-installSafety{border-radius:var(--dsw-radius-md);background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-label-secondary);align-items:flex-start;gap:8px;margin:0;padding:10px 12px;font-size:12px;line-height:18px;display:flex}
1465
+ .sgc-installSafety>svg{color:var(--dsw-alias-state-warn-primary);flex:none;margin-top:2px}
1466
+ .sgc-installSafetyText{flex-direction:column;gap:4px;min-width:0;display:flex}
1467
+ .sgc-wide{border-radius:var(--dsw-radius-md);justify-content:center;width:100%;height:40px}
1468
+ .sgc-inputError{color:var(--dsw-alias-state-error-primary);margin:-4px 0 0;font-size:12px;line-height:18px}
1469
+ .sgc-dialogActions{display:flex;justify-content:flex-end;gap:10px}
1470
+ `;
1398
1471
  function OutputBlock({ output }) {
1399
1472
  if (!output) return null;
1400
1473
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
@@ -1406,15 +1479,37 @@ window.__ModuleLoader__.load({
1406
1479
  children: output
1407
1480
  });
1408
1481
  }
1482
+ const REGISTRY_CHOICES = [
1483
+ {
1484
+ value: "",
1485
+ titleKey: "plugins.registryDefault"
1486
+ },
1487
+ {
1488
+ value: "official",
1489
+ titleKey: "plugins.registryOfficial"
1490
+ },
1491
+ {
1492
+ value: "npmmirror",
1493
+ titleKey: "plugins.registryNpmmirror"
1494
+ },
1495
+ {
1496
+ value: "custom",
1497
+ titleKey: "plugins.registryCustom"
1498
+ }
1499
+ ];
1409
1500
  function InstallModal({ open, onClose, onSettled }) {
1410
1501
  const { t, notify } = usePanel();
1411
1502
  const [spec, setSpec] = (0, react.useState)("");
1412
1503
  const [step, setStep] = (0, react.useState)({ kind: "editing" });
1413
1504
  const [guideOpen, setGuideOpen] = (0, react.useState)(false);
1414
- const [detailsOpen, setDetailsOpen] = (0, react.useState)(false);
1415
- const [lastOp, setLastOp] = (0, react.useState)(null);
1505
+ const [registryOpen, setRegistryOpen] = (0, react.useState)(false);
1416
1506
  const [registry, setRegistry] = (0, react.useState)("");
1417
1507
  const [customRegistry, setCustomRegistry] = (0, react.useState)("");
1508
+ const [customInvalid, setCustomInvalid] = (0, react.useState)(false);
1509
+ const [detailsOpen, setDetailsOpen] = (0, react.useState)(false);
1510
+ const [lastOp, setLastOp] = (0, react.useState)(null);
1511
+ const registryToggleRef = (0, react.useRef)(null);
1512
+ const [registryPos, setRegistryPos] = (0, react.useState)(null);
1418
1513
  const settled = (0, react.useRef)(onSettled);
1419
1514
  settled.current = onSettled;
1420
1515
  (0, react.useEffect)(() => {
@@ -1432,26 +1527,79 @@ window.__ModuleLoader__.load({
1432
1527
  };
1433
1528
  const timer = setInterval(() => {
1434
1529
  tick();
1435
- }, 1500);
1530
+ }, 800);
1436
1531
  tick();
1437
1532
  return () => {
1438
1533
  active = false;
1439
1534
  clearInterval(timer);
1440
1535
  };
1441
1536
  }, [step.kind]);
1537
+ (0, react.useEffect)(() => {
1538
+ if (!registryOpen) return;
1539
+ const place = () => {
1540
+ const el = registryToggleRef.current;
1541
+ if (!el) return;
1542
+ const rect = el.getBoundingClientRect();
1543
+ const width = Math.min(440, window.innerWidth - 24);
1544
+ const left = Math.max(12, Math.min(rect.left, window.innerWidth - width - 12));
1545
+ const top = Math.min(rect.bottom + 6, window.innerHeight - 200);
1546
+ setRegistryPos({
1547
+ left,
1548
+ top
1549
+ });
1550
+ };
1551
+ place();
1552
+ window.addEventListener("resize", place);
1553
+ window.addEventListener("scroll", place, true);
1554
+ return () => {
1555
+ window.removeEventListener("resize", place);
1556
+ window.removeEventListener("scroll", place, true);
1557
+ };
1558
+ }, [registryOpen]);
1559
+ (0, react.useEffect)(() => {
1560
+ if (!registryOpen) return;
1561
+ const onDoc = (event) => {
1562
+ const target = event.target;
1563
+ if (registryToggleRef.current?.contains(target)) return;
1564
+ if (target instanceof Element && target.closest("[data-sgc-registry]")) return;
1565
+ setRegistryOpen(false);
1566
+ };
1567
+ const onKey = (event) => {
1568
+ if (event.key === "Escape") setRegistryOpen(false);
1569
+ };
1570
+ document.addEventListener("mousedown", onDoc);
1571
+ document.addEventListener("keydown", onKey);
1572
+ return () => {
1573
+ document.removeEventListener("mousedown", onDoc);
1574
+ document.removeEventListener("keydown", onKey);
1575
+ };
1576
+ }, [registryOpen]);
1442
1577
  const run = async () => {
1443
1578
  let chosen = null;
1444
1579
  if (registry === "official") chosen = "https://registry.npmjs.org/";
1445
1580
  else if (registry === "npmmirror") chosen = "https://registry.npmmirror.com/";
1446
1581
  else if (registry === "custom") {
1447
- chosen = customRegistry.trim();
1448
- if (!/^https?:\/\/\S+$/.test(chosen)) {
1449
- notify(t("plugins.registryCustomInvalid"));
1582
+ const url = customRegistry.trim();
1583
+ if (!/^https?:\/\/\S+$/.test(url)) {
1584
+ setCustomInvalid(true);
1450
1585
  return;
1451
1586
  }
1587
+ chosen = url;
1452
1588
  }
1589
+ setRegistryOpen(false);
1453
1590
  setDetailsOpen(false);
1454
1591
  setLastOp(null);
1592
+ setStep({ kind: "checking" });
1593
+ try {
1594
+ const { problems } = await post("/superagent-console/api/plugins/check", { spec: spec.trim() });
1595
+ if (problems.length > 0) {
1596
+ setStep({
1597
+ kind: "refused",
1598
+ problems
1599
+ });
1600
+ return;
1601
+ }
1602
+ } catch {}
1455
1603
  setStep({ kind: "running" });
1456
1604
  try {
1457
1605
  await post("/superagent-console/api/plugins/install", {
@@ -1463,224 +1611,283 @@ window.__ModuleLoader__.load({
1463
1611
  setStep({ kind: "editing" });
1464
1612
  }
1465
1613
  };
1466
- const submit = (event) => {
1467
- event.preventDefault();
1468
- if (spec.trim() && step.kind === "editing") run();
1614
+ /** 立即启用: mount every bundle the add introduced, then surface the restart notice. */
1615
+ const enableNow = async () => {
1616
+ const bundles = lastOp?.installed?.filter((item) => item.bundle) ?? [];
1617
+ for (const item of bundles) try {
1618
+ await post("/superagent-console/api/plugins/enable", {
1619
+ name: item.name,
1620
+ enabled: true
1621
+ });
1622
+ } catch (failure) {
1623
+ notify(errorText(failure));
1624
+ return;
1625
+ }
1626
+ settled.current(true);
1627
+ onClose();
1469
1628
  };
1470
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Modal, {
1629
+ const editing = step.kind === "editing";
1630
+ const enableTargets = step.kind === "done" && (lastOp?.installed?.some((item) => item.bundle) ?? false);
1631
+ const chosenTitle = t((REGISTRY_CHOICES.find((choice) => choice.value === registry) ?? REGISTRY_CHOICES[0]).titleKey);
1632
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: SGC_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1471
1633
  open,
1472
1634
  onClose,
1473
- className: "dt-plugins-modal",
1635
+ className: "sgc-installDialog",
1636
+ contentClassName: "sgc-installContent",
1474
1637
  title: step.kind === "running" ? t("plugins.installingTitle") : step.kind === "failed" ? t("plugins.installFailedTitle") : t("plugins.installTitle"),
1475
1638
  closeLabel: t("plugins.installClose"),
1476
- description: step.kind === "editing" ? t("plugins.installDescription") : void 0,
1477
- footer: step.kind === "editing" ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1478
- step.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button$1, {
1479
- variant: "ghost",
1480
- onClick: onClose,
1481
- children: t("plugins.installClose")
1482
- }),
1483
- step.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button$1, {
1484
- variant: "ghost",
1485
- onClick: onClose,
1486
- children: t("plugins.installClose")
1487
- }),
1488
- step.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button$1, {
1639
+ description: editing ? t("plugins.installDescription") : void 0,
1640
+ footer: editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1641
+ className: "sgc-installFooter",
1642
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1643
+ className: "sgc-installSafety",
1644
+ role: "note",
1645
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {
1646
+ size: 14,
1647
+ "aria-hidden": "true"
1648
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1649
+ className: "sgc-installSafetyText",
1650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("plugins.installGuideSafety") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("plugins.installUpgradeNotice") })]
1651
+ })]
1652
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1489
1653
  variant: "primary",
1654
+ className: "sgc-wide",
1655
+ disabled: !spec.trim(),
1490
1656
  onClick: () => {
1491
1657
  run();
1492
1658
  },
1493
- children: t("plugins.installRetry")
1494
- }),
1495
- step.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button$1, {
1496
- variant: "primary",
1497
- onClick: onClose,
1498
- children: t("plugins.installClose")
1499
- })
1500
- ] }),
1659
+ children: t("plugins.installRun")
1660
+ })]
1661
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1662
+ className: "sgc-dialogActions",
1663
+ children: [
1664
+ (step.kind === "running" || step.kind === "failed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1665
+ variant: "ghost",
1666
+ onClick: onClose,
1667
+ children: t("plugins.installClose")
1668
+ }),
1669
+ step.kind === "refused" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1670
+ variant: "primary",
1671
+ onClick: () => setStep({ kind: "editing" }),
1672
+ children: t("plugins.installEdit")
1673
+ }),
1674
+ step.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1675
+ variant: "ghost",
1676
+ onClick: () => setStep({ kind: "editing" }),
1677
+ children: t("plugins.installChangeRegistry")
1678
+ }),
1679
+ step.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1680
+ variant: "primary",
1681
+ onClick: () => {
1682
+ run();
1683
+ },
1684
+ children: t("plugins.installRetry")
1685
+ }),
1686
+ step.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1687
+ variant: "ghost",
1688
+ onClick: onClose,
1689
+ children: t("plugins.installClose")
1690
+ }),
1691
+ step.kind === "done" && enableTargets && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1692
+ variant: "primary",
1693
+ onClick: () => {
1694
+ enableNow();
1695
+ },
1696
+ children: t("plugins.installEnableNow")
1697
+ })
1698
+ ]
1699
+ }),
1501
1700
  children: [
1502
- step.kind === "editing" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1503
- onSubmit: submit,
1701
+ editing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1702
+ className: "sgc-installBody",
1504
1703
  children: [
1505
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
1506
- type: "text",
1507
- value: spec,
1508
- placeholder: t("plugins.installSpecPlaceholder"),
1509
- "aria-label": t("plugins.installSpecLabel"),
1510
- style: { width: "100%" },
1511
- onChange: (event) => {
1512
- setSpec(event.target.value);
1513
- }
1704
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1705
+ className: "sgc-installField",
1706
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1707
+ type: "text",
1708
+ autoFocus: true,
1709
+ "data-modal-autofocus": true,
1710
+ value: spec,
1711
+ placeholder: t("plugins.installSpecPlaceholder"),
1712
+ "aria-label": t("plugins.installSpecLabel"),
1713
+ onChange: (event) => {
1714
+ setSpec(event.target.value);
1715
+ },
1716
+ onKeyDown: (event) => {
1717
+ if (event.nativeEvent.isComposing) return;
1718
+ if (event.key === "Enter" && spec.trim()) {
1719
+ event.preventDefault();
1720
+ run();
1721
+ }
1722
+ }
1723
+ })
1514
1724
  }),
1515
1725
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1516
- style: {
1517
- display: "flex",
1518
- alignItems: "center",
1519
- justifyContent: "space-between",
1520
- gap: 12,
1521
- marginTop: 12
1522
- },
1726
+ className: "sgc-optionsRow",
1523
1727
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1524
1728
  type: "button",
1525
- className: "dt-textlink",
1729
+ className: "sgc-guideToggle",
1526
1730
  "aria-expanded": guideOpen,
1527
- style: {
1528
- display: "inline-flex",
1529
- alignItems: "center",
1530
- gap: 4
1531
- },
1532
1731
  onClick: () => setGuideOpen(!guideOpen),
1533
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1534
- name: guideOpen ? "chevron-up" : "chevron-down",
1535
- size: 14
1536
- }), guideOpen ? t("plugins.installGuideHide") : t("plugins.installGuideToggle")]
1537
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1538
- style: {
1539
- display: "inline-flex",
1540
- alignItems: "center",
1541
- gap: 6,
1542
- border: "0.5px solid var(--dsw-alias-border-l4)",
1543
- borderRadius: 8,
1544
- padding: "0 10px",
1545
- height: 34
1546
- },
1547
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1548
- style: {
1549
- fontSize: 13,
1550
- opacity: .7,
1551
- whiteSpace: "nowrap"
1552
- },
1553
- children: t("plugins.registryTitle")
1554
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1555
- className: "dt-select",
1556
- style: {
1557
- border: "none",
1558
- background: "transparent",
1559
- width: "auto",
1560
- height: "auto",
1561
- padding: 0
1562
- },
1563
- "aria-label": t("plugins.registryTitle"),
1564
- value: registry,
1565
- onChange: (event) => setRegistry(event.target.value),
1566
- children: [
1567
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1568
- value: "",
1569
- children: t("plugins.registryDefault")
1570
- }),
1571
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1572
- value: "official",
1573
- children: t("plugins.registryOfficial")
1574
- }),
1575
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1576
- value: "npmmirror",
1577
- children: t("plugins.registryNpmmirror")
1578
- }),
1579
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1580
- value: "custom",
1581
- children: t("plugins.registryCustom")
1582
- })
1583
- ]
1584
- })]
1732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
1733
+ className: "sgc-guideChevron",
1734
+ size: 13,
1735
+ "aria-hidden": "true"
1736
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: guideOpen ? t("plugins.installGuideHide") : t("plugins.installGuideToggle") })]
1737
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1738
+ ref: registryToggleRef,
1739
+ type: "button",
1740
+ className: "sgc-registryToggle",
1741
+ "aria-expanded": registryOpen,
1742
+ "aria-haspopup": "dialog",
1743
+ onClick: () => setRegistryOpen(!registryOpen),
1744
+ children: [
1745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("plugins.registryTitle") }),
1746
+ " ",
1747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1748
+ className: "sgc-registryChosen",
1749
+ children: chosenTitle
1750
+ }),
1751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
1752
+ className: "sgc-guideChevron",
1753
+ size: 12,
1754
+ "aria-hidden": "true"
1755
+ })
1756
+ ]
1585
1757
  })]
1586
1758
  }),
1587
- registry === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1588
- style: { marginTop: 10 },
1589
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
1759
+ guideOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1760
+ className: "sgc-guide",
1761
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1762
+ className: "sgc-guideList",
1763
+ children: GUIDE_ITEMS.map(({ key, example, titleKey, hintKey }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1764
+ className: "sgc-guideItem",
1765
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1766
+ className: "sgc-guideMain",
1767
+ children: [
1768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1769
+ className: "sgc-guideTitle",
1770
+ children: t(titleKey)
1771
+ }),
1772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1773
+ className: "sgc-guideHint",
1774
+ children: t(hintKey)
1775
+ }),
1776
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1777
+ className: "sgc-guideExample",
1778
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1779
+ className: "sgc-guideExampleLabel",
1780
+ children: t("plugins.installGuideExampleLabel")
1781
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: example })]
1782
+ })
1783
+ ]
1784
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1785
+ variant: "outline",
1786
+ size: "sm",
1787
+ "aria-label": t("plugins.fillExample", { example }),
1788
+ onClick: () => setSpec(example),
1789
+ children: t("plugins.installGuideFill")
1790
+ })]
1791
+ }, key))
1792
+ })
1793
+ }),
1794
+ registryOpen && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1795
+ className: "sgc-registry",
1796
+ "data-sgc-registry": true,
1797
+ "aria-label": t("plugins.registryLegend"),
1798
+ style: registryPos ?? {
1799
+ visibility: "hidden",
1800
+ left: 0,
1801
+ top: 0
1802
+ },
1803
+ children: [REGISTRY_CHOICES.map(({ value, titleKey }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1804
+ className: "sgc-registryOption",
1805
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1806
+ type: "radio",
1807
+ name: "sgc-registry-choice",
1808
+ checked: registry === value,
1809
+ onChange: () => {
1810
+ setRegistry(value);
1811
+ if (value !== "custom") setCustomInvalid(false);
1812
+ }
1813
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1814
+ className: "sgc-registryTitle",
1815
+ children: t(titleKey)
1816
+ })]
1817
+ }, value || "default")), registry === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1590
1818
  type: "text",
1819
+ className: "sgc-registryCustomField",
1591
1820
  value: customRegistry,
1592
1821
  placeholder: t("plugins.registryCustomPlaceholder"),
1822
+ "aria-invalid": customInvalid || void 0,
1593
1823
  "aria-label": t("plugins.registryCustom"),
1594
- style: { width: "100%" },
1595
1824
  onChange: (event) => {
1596
1825
  setCustomRegistry(event.target.value);
1826
+ setCustomInvalid(false);
1597
1827
  }
1598
1828
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1599
- className: "dt-hint",
1600
- children: t("plugins.registryCustomHint")
1601
- })]
1602
- }),
1603
- guideOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1604
- className: "dt-disclosure-body",
1605
- style: { marginTop: 12 },
1606
- children: GUIDE_EXAMPLES.map(({ key, example }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1607
- className: "dt-row",
1608
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "idle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1609
- className: "dt-row-main",
1610
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1611
- className: "dt-row-title dt-line-text",
1612
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GuideText, { kind: key })
1613
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1614
- className: "dt-row-meta",
1615
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1616
- className: "dt-code",
1617
- children: example
1618
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1619
- type: "button",
1620
- className: "dt-textlink",
1621
- "aria-label": t("plugins.fillExample", { example }),
1622
- onClick: () => setSpec(example),
1623
- children: t("plugins.fill")
1624
- })]
1625
- })]
1626
- })]
1627
- }, key))
1628
- }),
1629
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1630
- className: "dt-banner",
1631
- style: { marginTop: 12 },
1632
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1633
- style: {
1634
- display: "inline-flex",
1635
- alignItems: "flex-start",
1636
- gap: 6
1637
- },
1638
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
1639
- name: "alert",
1640
- size: 14
1641
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("plugins.installGuideSafety") })]
1642
- })
1643
- }),
1644
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button$1, {
1645
- variant: "primary",
1646
- style: {
1647
- width: "100%",
1648
- marginTop: 14,
1649
- display: "flex",
1650
- alignItems: "center",
1651
- justifyContent: "center"
1652
- },
1653
- disabled: !spec.trim(),
1654
- onClick: () => {
1655
- run();
1656
- },
1657
- children: t("plugins.installRun")
1658
- })
1829
+ className: customInvalid ? "sgc-inputError sgc-registryCustomError" : "sgc-registryHint sgc-registryCustomError",
1830
+ children: customInvalid ? t("plugins.registryCustomInvalid") : t("plugins.registryCustomHint")
1831
+ })] })]
1832
+ }), document.body)
1659
1833
  ]
1660
1834
  }),
1835
+ step.kind === "checking" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1836
+ className: "dt-row",
1837
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "ongoing" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1838
+ className: "dt-row-main",
1839
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1840
+ className: "dt-row-title",
1841
+ children: t("plugins.installChecking")
1842
+ })
1843
+ })]
1844
+ }),
1845
+ step.kind === "refused" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1846
+ className: "dt-row",
1847
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "error" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1848
+ className: "dt-row-main",
1849
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1850
+ className: "dt-row-title",
1851
+ children: t("plugins.installConfirmedRefused")
1852
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1853
+ className: "dt-row-meta",
1854
+ children: step.problems.map((problem, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1855
+ problem.kind === "invalid" && t("plugins.installProblemInvalid", { reason: problem.detail ?? "" }),
1856
+ problem.kind === "installed" && t("plugins.installProblemInstalled"),
1857
+ problem.kind === "links" && t("plugins.problemLinks")
1858
+ ] }, index))
1859
+ })]
1860
+ })]
1861
+ }) }),
1661
1862
  step.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1662
1863
  className: "dt-row",
1663
1864
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "ongoing" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1664
1865
  className: "dt-row-main",
1665
1866
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1666
1867
  className: "dt-row-title",
1667
- children: t("plugins.installingTitle")
1868
+ children: (lastOp?.output ?? "") === "" ? t("plugins.installStarting") : t("plugins.installingTitle")
1668
1869
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1669
1870
  className: "dt-row-meta",
1670
1871
  children: t("plugins.installBackground")
1671
1872
  })]
1672
1873
  })]
1673
1874
  }), lastOp?.output && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutputBlock, { output: lastOp.output })] }),
1674
- step.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1875
+ step.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1675
1876
  className: "dt-row",
1676
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "done" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1877
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "done" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1677
1878
  className: "dt-row-main",
1678
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1679
1880
  className: "dt-row-title",
1680
- children: t("plugins.installDoneRestart")
1681
- })
1881
+ children: t("plugins.installedTitle")
1882
+ }), (lastOp?.installed?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1883
+ className: "dt-row-meta",
1884
+ children: lastOp.installed.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [item.name, item.version ? " · " + t("plugins.installVersion", { version: item.version }) : ""] }, item.name))
1885
+ })]
1682
1886
  })]
1683
- }),
1887
+ }), lastOp?.profileDir && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1888
+ className: "dt-row-meta",
1889
+ children: t("plugins.installLocation", { dir: lastOp.profileDir })
1890
+ })] }),
1684
1891
  step.kind === "failed" && lastOp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1685
1892
  className: "dt-row",
1686
1893
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, { state: "error" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1705,7 +1912,7 @@ window.__ModuleLoader__.load({
1705
1912
  })]
1706
1913
  }) })
1707
1914
  ]
1708
- });
1915
+ })] });
1709
1916
  }
1710
1917
  function PluginItem({ row, busy, onUninstall, onToggle }) {
1711
1918
  const { t } = usePanel();
@@ -1810,7 +2017,6 @@ window.__ModuleLoader__.load({
1810
2017
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1811
2018
  className: "dt-form",
1812
2019
  children: [
1813
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ".dt-plugins-modal.dt-modal { width: min(680px, 100%); }" }),
1814
2020
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1815
2021
  className: "dt-status-head",
1816
2022
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
@@ -4895,7 +5101,7 @@ window.__ModuleLoader__.load({
4895
5101
  "plugins.uninstallTitle": ["卸载「{name}」?", "Uninstall \"{name}\"?"],
4896
5102
  "plugins.uninstallDescription": ["卸载后它提供的功能会消失,重启引擎后生效。", "Its features disappear; the change applies when the engine restarts."],
4897
5103
  "plugins.installTitle": ["添加插件", "Add plugin"],
4898
- "plugins.installDescription": ["输入插件的包名、Git 仓库地址或本地目录路径。", "Enter the plugin's package name, Git repository, or local directory path."],
5104
+ "plugins.installDescription": ["输入插件的包名、GitHub 仓库地址或本地目录路径。", "Enter the plugin's package name, GitHub repository address, or local directory path."],
4899
5105
  "plugins.installSpecLabel": ["包名或地址", "Package or address"],
4900
5106
  "plugins.installSpecPlaceholder": ["例如 dsh-plugin-whale-pet", "e.g. dsh-plugin-whale-pet"],
4901
5107
  "plugins.registryTitle": ["安装源", "Registry"],
@@ -4904,16 +5110,22 @@ window.__ModuleLoader__.load({
4904
5110
  "plugins.registryNpmmirror": ["中国大陆镜像源", "China mirror registry"],
4905
5111
  "plugins.registryCustom": ["自定义地址", "Custom URL"],
4906
5112
  "plugins.registryCustomPlaceholder": ["https://npm.example.com/", "https://npm.example.com/"],
4907
- "plugins.registryCustomHint": ["自定义源需要登录时,把凭据放在本机的 ~/.npmrc 里。", "For a registry that requires login, keep its credentials in the local ~/.npmrc."],
5113
+ "plugins.registryCustomHint": ["填写内网或私有 npm 源地址,以 http:// 或 https:// 开头。若为需要登录的源,请把凭据放在本机的 ~/.npmrc 里。", "Enter an internal or private npm registry address starting with http:// or https://. If it requires a login, store the credentials in ~/.npmrc on this machine."],
4908
5114
  "plugins.registryCustomInvalid": ["请输入以 http:// 或 https:// 开头的地址", "Enter an address starting with http:// or https://"],
4909
5115
  "plugins.installGuideToggle": ["插件安装引导和示例", "Install guide and examples"],
4910
5116
  "plugins.installGuideHide": ["收起引导", "Hide guide"],
4911
- "plugins.installGuideNpm": ["npm 包名:插件包名即 npm 包名(如 dsh-xxx 或 @作者/插件名)。", "npm package: the plugin name is its npm name (dsh-xxx or @author/plugin)."],
4912
- "plugins.installGuideGit": ["Git 仓库:github:owner/repo、git URL 或 .git 地址。", "Git repository: github:owner/repo, a git URL, or a .git address."],
4913
- "plugins.installGuidePath": ["本地目录:本机插件 checkout 的绝对路径(以 link 方式安装)。", "Local directory: an absolute path to a local checkout (installed as a link)."],
4914
- "plugins.fill": ["填入", "Fill in"],
5117
+ "plugins.installGuideNpm": ["npm 包名", "npm package name"],
5118
+ "plugins.installGuideNpmHint": ["插件包名即 npm 包名(如 dsh-xxx 或 @作者/插件名)。", "The plugin name is its npm name (dsh-xxx or @author/plugin)."],
5119
+ "plugins.installGuideGit": ["Git 仓库", "Git repository"],
5120
+ "plugins.installGuideGitHint": ["github:owner/repo、git URL 或 .git 地址。", "github:owner/repo, a git URL, or a .git address."],
5121
+ "plugins.installGuidePath": ["本地目录", "Local directory"],
5122
+ "plugins.installGuidePathHint": ["本机插件 checkout 的绝对路径(以 link 方式安装)。", "An absolute path to a local checkout (installed as a link)."],
5123
+ "plugins.installGuideExampleLabel": ["示例:", "Example:"],
5124
+ "plugins.installGuideFill": ["填入示例", "Use example"],
4915
5125
  "plugins.fillExample": ["填入示例 {example}", "Fill in the example {example}"],
4916
- "plugins.installGuideSafety": ["请确认插件来源可信。插件以你的权限在本机运行,来源不明的插件可能损坏引擎,或读取和泄露你的数据。", "Only add plugins you trust. Plugins run locally with your permissions; untrusted plugins can damage the engine or read and leak your data."],
5126
+ "plugins.installGuideSafety": ["请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。", "Install only plugins you trust: they run with your permissions and can damage DeepSeek Harness or leak your data."],
5127
+ "plugins.installUpgradeNotice": ["按需安装的官方插件会在版本与 DSH 不同时提供“更新”。其他插件请先卸载再安装新版。", "On-demand Official plugins offer Update when their version differs from DSH. To update other plugins, uninstall and reinstall them."],
5128
+ "plugins.registryLegend": ["从哪个 npm 源下载插件", "The npm registry the plugin is downloaded from"],
4917
5129
  "plugins.installRun": ["安装", "Install"],
4918
5130
  "plugins.installingTitle": ["插件安装中…", "Installing plugin…"],
4919
5131
  "plugins.installBackground": ["可以关闭此窗口,安装会在后台继续;进度显示在列表上方的横幅里。", "You can close this dialog; the install continues in the background and its progress shows in the banner above the list."],
@@ -4923,6 +5135,18 @@ window.__ModuleLoader__.load({
4923
5135
  "plugins.installDetailsHide": ["收起安装详情", "Hide install details"],
4924
5136
  "plugins.installRetry": ["重试", "Retry"],
4925
5137
  "plugins.installClose": ["完成", "Done"],
5138
+ "plugins.installChecking": ["正在检查…", "Checking…"],
5139
+ "plugins.installStarting": ["正在准备安装…", "Preparing installation…"],
5140
+ "plugins.installedTitle": ["已安装", "Installed"],
5141
+ "plugins.installEnableNow": ["立即启用", "Enable now"],
5142
+ "plugins.installVersion": ["版本 {version}", "Version {version}"],
5143
+ "plugins.installLocation": ["安装位置:{dir}", "Installs into {dir}"],
5144
+ "plugins.installChangeRegistry": ["更换安装源", "Change registry"],
5145
+ "plugins.installConfirmedRefused": ["无法安装此插件", "This plugin cannot be installed"],
5146
+ "plugins.installProblemInstalled": ["该插件已安装。如需升级,请卸载后重新安装", "This plugin is already installed. To upgrade it, uninstall it and install it again"],
5147
+ "plugins.installProblemInvalid": ["无法识别这个包名或地址:{reason}", "This is not a package name or address that can be installed: {reason}"],
5148
+ "plugins.installEdit": ["编辑", "Edit"],
5149
+ "plugins.problemLinks": ["引擎 profile 的 link: 依赖目标不存在,请先修复后再安装。", "A link dependency of the engine profile points to a missing target; repair it before installing."],
4926
5150
  "plugins.failure.network": ["网络连接失败,请检查网络或代理后重试", "Network failed; check connectivity or proxy, then retry"],
4927
5151
  "plugins.failure.notFound": ["未找到相关插件", "Plugin not found"],
4928
5152
  "plugins.failure.version": ["没有匹配的版本", "No matching version"],
package/lib/index.d.ts CHANGED
@@ -88,6 +88,16 @@ interface EngineActionResult {
88
88
  }
89
89
  /** Run the lifecycle script's start/restart action and wait for its verdict. */
90
90
  declare function engineAction(config: ResolvedConsoleConfig, action: 'start' | 'restart', env?: InstallEnv): Promise<EngineActionResult>;
91
+ /**
92
+ * Collect `link:` dependencies whose target no longer exists. The official
93
+ * CLI sails through the package operation itself (pnpm trusts the lockfile's
94
+ * recorded link resolution without checking the target) but its post-install
95
+ * bundle reconciliation probes `node_modules/<name>/package.json` and exits 1
96
+ * with an opaque "cannot resolve profile bundle" error — the shape seen after
97
+ * a hosting checkout is moved and only the manifest specifiers are rewritten.
98
+ * Refusing up front keeps the failure readable and names the repair.
99
+ */
100
+ declare function danglingLinks(config: ResolvedConsoleConfig): string[];
91
101
  /** Minimal structural view of the settings service the sync reads through. */
92
102
  interface SettingsFace {
93
103
  describe(): Array<{
@@ -165,4 +175,4 @@ declare function proxyHandler(options: ProxyOptions): (req: IncomingMessage, res
165
175
  declare const name: string;
166
176
  declare const inject: string[];
167
177
  declare function apply(ctx: Context, config?: Partial<ResolvedConsoleConfig>): void;
168
- export { Config, type ConsoleState, type DetectResult, type Prereq, type ProviderDiff, type ResolvedConsoleConfig, type SettingsFace, type SetupState, type StageId, type SyncMode, type SyncReport, apply, applyProviderSync, detect, diffProviders, engineAction, inject, name, proxyHandler, readEngineProviders, readHostProviders, readSetupState, runInstall };
178
+ export { Config, type ConsoleState, type DetectResult, type Prereq, type ProviderDiff, type ResolvedConsoleConfig, type SettingsFace, type SetupState, type StageId, type SyncMode, type SyncReport, apply, applyProviderSync, danglingLinks, detect, diffProviders, engineAction, inject, name, proxyHandler, readEngineProviders, readHostProviders, readSetupState, runInstall };
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import fs, { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
- import { execFile } from "node:child_process";
5
+ import { execFile, spawn } from "node:child_process";
6
6
  import { parse, stringify } from "yaml";
7
7
  import { Readable } from "node:stream";
8
8
  // src/config.ts
@@ -92,6 +92,73 @@ function run(argv, options = {}) {
92
92
  });
93
93
  });
94
94
  }
95
+ /**
96
+ * Spawn like {@link run} but deliver output as it arrives: the plugins API
97
+ * mirrors each chunk into the operation state so the dialog can stream the
98
+ * install log the way the native manager does. Output stays bounded by the
99
+ * consumer (it keeps a tail); this runner only concatenates what it saw.
100
+ */
101
+ function runStreaming(argv, options = {}, onChunk) {
102
+ const [file, ...args] = argv;
103
+ if (!file) throw new Error("runStreaming() needs a command");
104
+ const { cwd, timeoutMs = 3e4, extraPath } = options;
105
+ const child = spawn(file, args, {
106
+ cwd,
107
+ stdio: [
108
+ "ignore",
109
+ "pipe",
110
+ "pipe"
111
+ ],
112
+ env: {
113
+ ...process.env,
114
+ PATH: spawnPath(extraPath)
115
+ },
116
+ windowsHide: true
117
+ });
118
+ let stdout = "";
119
+ let stderr = "";
120
+ let timedOut = false;
121
+ const timer = setTimeout(() => {
122
+ timedOut = true;
123
+ child.kill("SIGKILL");
124
+ }, timeoutMs);
125
+ child.stdout?.on("data", (chunk) => {
126
+ stdout += chunk.toString();
127
+ onChunk?.(chunk.toString());
128
+ });
129
+ child.stderr?.on("data", (chunk) => {
130
+ stderr += chunk.toString();
131
+ onChunk?.(chunk.toString());
132
+ });
133
+ return {
134
+ promise: new Promise((resolve) => {
135
+ const startedAt = Date.now();
136
+ child.on("error", (error) => {
137
+ clearTimeout(timer);
138
+ resolve({
139
+ code: -1,
140
+ stdout,
141
+ stderr: stderr + String(error),
142
+ timedOut
143
+ });
144
+ });
145
+ child.on("close", (code) => {
146
+ clearTimeout(timer);
147
+ const hitTimeout = timedOut && Date.now() - startedAt >= timeoutMs - 1;
148
+ resolve({
149
+ code: code ?? (timedOut || hitTimeout ? -1 : 0),
150
+ stdout,
151
+ stderr,
152
+ timedOut: timedOut || hitTimeout
153
+ });
154
+ });
155
+ }),
156
+ kill: () => {
157
+ clearTimeout(timer);
158
+ child.kill("SIGKILL");
159
+ }
160
+ };
161
+ }
95
162
  /** Truncate captured output for audit logging. */
96
163
  function tail$1(text, max = 2e3) {
97
164
  const value = text.trim();
@@ -328,6 +395,11 @@ async function detect(config, env = {}) {
328
395
  * own host profile);
329
396
  * - install specs must be non-empty, option-free single tokens; remove
330
397
  * names must already be profile dependencies;
398
+ * - before any install/remove, `link:` dependencies are realpath-checked;
399
+ * a dangling target (checkout moved, manifest rewritten without
400
+ * reinstalling) makes the official CLI's post-install bundle
401
+ * reconciliation exit 1 with an opaque resolve error, so the operation
402
+ * refuses up front and names the repair;
331
403
  * - the engine core package (`dsh-superagent`) can be neither removed
332
404
  * nor toggled — without it the console has no subject;
333
405
  * - operations are single-flight in-process: install, remove, and
@@ -433,6 +505,71 @@ function validateRegistry(raw) {
433
505
  if (!/^https?:\/\/\S+$/.test(registry) || registry.length > 256) throw new PluginRequestError("invalid registry: an http(s) URL is required");
434
506
  return registry;
435
507
  }
508
+ /**
509
+ * Collect `link:` dependencies whose target no longer exists. The official
510
+ * CLI sails through the package operation itself (pnpm trusts the lockfile's
511
+ * recorded link resolution without checking the target) but its post-install
512
+ * bundle reconciliation probes `node_modules/<name>/package.json` and exits 1
513
+ * with an opaque "cannot resolve profile bundle" error — the shape seen after
514
+ * a hosting checkout is moved and only the manifest specifiers are rewritten.
515
+ * Refusing up front keeps the failure readable and names the repair.
516
+ */
517
+ function danglingLinks(config) {
518
+ const manifest = readManifest(config.profileName);
519
+ if (!manifest) return [];
520
+ const dir = profileDir(config.profileName);
521
+ const broken = [];
522
+ for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
523
+ if (!spec.startsWith("link:")) continue;
524
+ const target = spec.slice(5);
525
+ const resolved = path.isAbsolute(target) ? target : path.resolve(dir, target);
526
+ if (!existsSync(resolved)) broken.push(`${name} (${spec})`);
527
+ }
528
+ return broken;
529
+ }
530
+ /** Package-name reading of an install spec, when derivable offline. */
531
+ function specPackageName(config, spec) {
532
+ const trimmed = spec.trim();
533
+ if (trimmed.startsWith("link:") || trimmed.startsWith("file:")) {
534
+ const target = trimmed.slice(trimmed.indexOf(":") + 1);
535
+ const resolved = path.isAbsolute(target) ? target : path.resolve(profileDir(config.profileName), target);
536
+ try {
537
+ const manifest = JSON.parse(readFileSync(path.join(resolved, "package.json"), "utf8"));
538
+ return typeof manifest.name === "string" ? manifest.name : null;
539
+ } catch {
540
+ return null;
541
+ }
542
+ }
543
+ if (/^https?:\/\//.test(trimmed) || trimmed.startsWith("git")) return null;
544
+ if (trimmed.startsWith("@")) {
545
+ const at = trimmed.indexOf("@", 1);
546
+ return at === -1 ? trimmed : trimmed.slice(0, at);
547
+ }
548
+ const at = trimmed.indexOf("@");
549
+ return at === -1 ? trimmed : trimmed.slice(0, at);
550
+ }
551
+ /**
552
+ * Refuse installs before they start when the spec is malformed, already a
553
+ * profile dependency, or the profile carries dangling link: dependencies
554
+ * (the official CLI's post-install reconciliation exits 1 on those). Mirrors
555
+ * the native manager's checking phase and its refusal screen.
556
+ */
557
+ function checkProblems(config, raw) {
558
+ const spec = typeof raw === "string" ? raw.trim() : "";
559
+ if (!spec || spec.length > 512 || /[\r\n]/.test(spec) || spec.startsWith("-")) return [{
560
+ kind: "invalid",
561
+ detail: "a non-empty package name, git URL, tarball, or absolute path is required"
562
+ }];
563
+ const problems = [];
564
+ const broken = danglingLinks(config);
565
+ if (broken.length > 0) problems.push({
566
+ kind: "links",
567
+ detail: broken.join(", ")
568
+ });
569
+ const name = specPackageName(config, spec);
570
+ if (name && Object.hasOwn(readManifest(config.profileName)?.dependencies ?? {}, name)) problems.push({ kind: "installed" });
571
+ return problems;
572
+ }
436
573
  /** Validate a remove/enable target: an existing profile dependency, not the engine core. */
437
574
  function validateName(config, raw, allowProtected = false) {
438
575
  const name = typeof raw === "string" ? raw.trim() : "";
@@ -460,12 +597,29 @@ async function runCliOperation(config, env, kind, target, registryUrl) {
460
597
  target: redact$1(target),
461
598
  ...registryUrl ? { registry: registryUrl } : {}
462
599
  });
463
- const result = await run(argv, {
600
+ const before = Object.keys(readManifest(config.profileName)?.dependencies ?? {});
601
+ let output = "";
602
+ const result = await runStreaming(argv, {
464
603
  timeoutMs: kind === "install" ? 9e5 : 6e5,
465
604
  extraPath: env.extraPath
466
- });
467
- const output = tail$1(redact$1((result.stdout + "\n" + result.stderr).trim()), 4e3);
605
+ }, (chunk) => {
606
+ output = (output + chunk).slice(-4e3);
607
+ last = {
608
+ ...last,
609
+ output: redact$1(output.trim())
610
+ };
611
+ }).promise;
468
612
  const failed = result.code !== 0;
613
+ let installed;
614
+ if (!failed && kind === "install") {
615
+ const after = readManifest(config.profileName)?.dependencies ?? {};
616
+ installed = Object.entries(after).filter(([name]) => !before.includes(name)).map(([name, spec]) => ({
617
+ name,
618
+ spec,
619
+ version: readInstalledManifest(config.profileName, name)?.version ?? null,
620
+ bundle: readInstalledManifest(config.profileName, name)?.dsh?.bundle !== void 0
621
+ }));
622
+ }
469
623
  const state = {
470
624
  running: false,
471
625
  kind,
@@ -474,8 +628,10 @@ async function runCliOperation(config, env, kind, target, registryUrl) {
474
628
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
475
629
  exitCode: result.code,
476
630
  timedOut: result.timedOut,
477
- output,
478
- error: failed ? result.timedOut ? "command timed out" : `exit ${result.code}` : null
631
+ output: tail$1(redact$1((result.stdout + "\n" + result.stderr).trim()), 4e3),
632
+ error: failed ? result.timedOut ? "command timed out" : `exit ${result.code}` : null,
633
+ installed,
634
+ profileDir: profileDir(config.profileName)
479
635
  };
480
636
  last = state;
481
637
  audit$1(config, {
@@ -496,6 +652,8 @@ async function runCliOperation(config, env, kind, target, registryUrl) {
496
652
  function runPluginOperation(config, env, kind, target, registry) {
497
653
  if (config.profileName === "desktop") return Promise.reject(new PluginRequestError("refusing to manage plugins of the desktop profile"));
498
654
  if (inFlight$1) return Promise.reject(new PluginRequestError("another plugin operation is already running"));
655
+ const broken = danglingLinks(config);
656
+ if (broken.length > 0) return Promise.reject(new PluginRequestError(`dangling link dependencies in the ${config.profileName} profile — the official installer's bundle reconciliation would fail; repair them first:\n ` + broken.join("\n ") + `\nFix: correct the targets in package.json and the pnpm-lock version fields, then run 'dsh plugin --profile ${config.profileName} install'.`));
499
657
  const prepared = kind === "install" ? validateSpec(target) : validateName(config, target);
500
658
  const registryUrl = kind === "install" ? validateRegistry(registry) : null;
501
659
  last = {
@@ -1530,6 +1688,11 @@ function consoleApiHandler(options) {
1530
1688
  case "GET /superagent-console/api/plugins/operation":
1531
1689
  sendJson(res, 200, pluginOperation());
1532
1690
  return;
1691
+ case "POST /superagent-console/api/plugins/check": {
1692
+ const body = await readJson(req);
1693
+ sendJson(res, 200, { problems: checkProblems(config, body.spec) });
1694
+ return;
1695
+ }
1533
1696
  case "POST /superagent-console/api/plugins/install": {
1534
1697
  const body = await readJson(req);
1535
1698
  if (pluginBusy()) {
@@ -1756,4 +1919,4 @@ function apply(ctx, config) {
1756
1919
  ctx.logger?.info?.("dsh-superagent-console: provider sync source attached (autoSync %s, mode %s)", resolved.autoSync ? "on" : "off", resolved.syncMode);
1757
1920
  });
1758
1921
  }
1759
- export { Config, apply, applyProviderSync, detect, diffProviders, engineAction, inject, name, proxyHandler, readEngineProviders, readHostProviders, readSetupState, runInstall };
1922
+ export { Config, apply, applyProviderSync, danglingLinks, detect, diffProviders, engineAction, inject, name, proxyHandler, readEngineProviders, readHostProviders, readSetupState, runInstall };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-superagent-console",
3
- "description": "Desktop console for the Superagent engine: the official panel embedded over a same-origin proxy. Detects the engine's state, installs it online (source build into a dedicated profile) when missing, and starts/restarts it on demand; the engine stays resident across desktop restarts.",
4
- "version": "0.1.2",
3
+ "description": "Desktop console for the dsh-superagent engine: same-origin proxied panel with engine detection, guided online install, plugin management, and model-provider sync.",
4
+ "version": "0.1.3",
5
5
  "license": "MIT",
6
6
  "author": "Shuai-S",
7
7
  "type": "module",
@@ -12,7 +12,9 @@
12
12
  "types": "./lib/index.d.ts",
13
13
  "default": "./lib/index.js"
14
14
  },
15
- "./client": "./lib/client.js"
15
+ "./client": "./lib/client.js",
16
+ "./locale/*": "./locale/*",
17
+ "./package.json": "./package.json"
16
18
  },
17
19
  "files": [
18
20
  "lib",
@@ -71,6 +73,7 @@
71
73
  "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
72
74
  "@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.2",
73
75
  "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.2",
76
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
74
77
  "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.2",
75
78
  "@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.2",
76
79
  "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-alpha.2",