dsh-m 0.3.0 → 0.4.1
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.en.md +21 -5
- package/README.md +21 -5
- package/docs/DESIGN.md +11 -1
- package/lib/cli.js +41 -8
- package/lib/client.js +230 -14
- package/lib/core/build-approval.js +88 -0
- package/lib/core/compat-check.js +56 -0
- package/lib/core/dsh-cli.js +34 -7
- package/lib/core/enablement.js +92 -0
- package/lib/core/host-api.js +35 -3
- package/lib/core/installed.js +9 -0
- package/lib/core/market.js +76 -3
- package/lib/core/patch-yaml.js +143 -0
- package/lib/core/profile-transaction.js +30 -2
- package/lib/core/registry.js +32 -1
- package/lib/core/toggle.js +220 -0
- package/lib/core/versions.js +18 -5
- package/lib/host.js +10 -1
- package/lib/tools.js +69 -5
- package/package.json +7 -1
- package/registry.json +128 -22
package/README.en.md
CHANGED
|
@@ -37,21 +37,36 @@ The default registry includes DSH Skins, ModSearch, the Lark / QQ / Weixin / WeC
|
|
|
37
37
|
| View | Capabilities |
|
|
38
38
|
|---|---|
|
|
39
39
|
| **Market** | Card flow with search and category filters (server-side filtering + pagination, 50 per page — even a 1,000-entry registry probes the current page only); expandable details; "Install" — npm sources pin the latest exact version, GitHub sources pin the release/tag commit |
|
|
40
|
-
| **Installed** | What your web profile actually has, annotated "via market / non-market"; update badges, upgrade, two-step confirm uninstall; 📖 README preview (64KB cap) |
|
|
40
|
+
| **Installed** | What your web profile actually has, annotated "via market / non-market"; update badges, upgrade, two-step confirm uninstall; 📖 README preview (64KB cap); **live phase badge** (●active / ●failed / ○pending) and a **one-click toggle** (0.4.0: delegates to the official pluginManager service for live application; falls back to file-level edits + restart notice when absent; dsh-m itself and official host lifelines are locked) |
|
|
41
41
|
| **Settings** | Registry address draft with "Validate & apply / Restore default / Download default registry.json / Check entries reachability"; configured vs active address and status at a glance; force refresh; dsh-m self-update |
|
|
42
42
|
|
|
43
43
|
After any mutation, the already-open Market and Installed views refresh the profile state together, keeping badges and cards in sync without closing and reopening the marketplace; a "⚡ Restart" banner appears — under systemd, the DSH launcher's `appExit` hook hands the restart back to a unit configured with `Restart=on-failure` or `Restart=always`, avoiding a `systemctl` helper inside the unit cgroup that is about to stop; if `appExit` is unavailable, the fallback uses a manager-owned transient `systemd-run` service and only then a detached helper. The client confirms the replacement by boot id and dismisses the banner, leaving DSH Web's own background connection recovery in control; it does not force a full-page reload during the auth/route handoff. The current DSH Web `0.1.5-rc.1` runtime has now live-loaded dsh-m `0.2.11`; `/dshm` ping returned version `0.2.11`, and an authenticated page request completed `303 → 200`. The reported 404 was not reproducible once the service stabilized; journal evidence showed repeated `status=75/TEMPFAIL` restarts during the incident. Live DSH `0.1.2-rc.1`, transient-fallback, and repeated install/uninstall E2E experiments remain pre-release gaps. Installs stream live pnpm progress (resolve → download → link → build).
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
### New in 0.4.0
|
|
46
|
+
|
|
47
|
+
- **Enablement toggle**: one-click on/off per installed card; internally routed to a row override (single-row plugins, applies live) or bundle selection (multi-row); the write path delegates to the official `pluginManager` service and falls back to direct loader operations when absent ([ADR-0001](./docs/adr/0001-delegate-with-fallback-for-plugin-manager.md)).
|
|
48
|
+
- **Live phase badges**: a projection of loader fiber state — failed plugins are visible at a glance.
|
|
49
|
+
- **Protection roster**: `dsh-m` itself plus the 16 official host lifeline modules cannot be toggled or uninstalled (upgrades unaffected).
|
|
50
|
+
- **Precise build approval**: when pnpm blocks build scripts, only the pending list is approved key-by-key; the allow-everything fallback is labeled honestly ([ADR-0002](./docs/adr/0002-precise-build-approval.md)).
|
|
51
|
+
- **Peer compatibility precheck**: install/upgrade validates `@deepseek-ai/dsh(-*)` peers against the runtime version before touching the profile (GitHub sources state the check was skipped); on mismatch the GUI asks, the agent tool returns structured data, the CLI takes `--force`.
|
|
52
|
+
- **Verified runtimes**: an optional `verified` array per registry entry records DSH runtimes actually tested — a claim of record, not a prediction; display-only, never gates installs.
|
|
53
|
+
- **Bundle identity check**: post-install warning when a package lands without a patch layer ("installed as a plain dependency").
|
|
54
|
+
|
|
55
|
+
### Retired in 0.4.x
|
|
56
|
+
|
|
57
|
+
- **Metadata source probe**: 0.4.0 introduced an npmjs / npmmirror ping race to pick the metadata read source; it has been removed entirely (including the `probeEnabled` / `probeTimeoutMs` / `probeCacheTtlMin` settings and the settings-page display). The official counterpart probe only pre-selects a registry in the interactive install dialog, which dsh-m does not have, and npmjs measured consistently faster from the host, so the probe always equaled the default. Metadata reads now always use npmjs, matching the install path (profile `.npmrc` default).
|
|
58
|
+
|
|
59
|
+
## Agent tools (8)
|
|
46
60
|
|
|
47
61
|
| Tool | Purpose |
|
|
48
62
|
|---|---|
|
|
49
63
|
| `dshm_search` | Search the curated registry (renders cards in chat) |
|
|
50
64
|
| `dshm_list` | List installed plugins (market / non-market annotated) |
|
|
51
|
-
| `dshm_install` | Install by listing id |
|
|
65
|
+
| `dshm_install` | Install by listing id (peer compatibility precheck before install; incompatible results return structured data for user confirmation, then `force` retries) |
|
|
52
66
|
| `dshm_uninstall` | Uninstall (confirm first; data kept, leftovers reported) |
|
|
53
67
|
| `dshm_outdated` | Check for newer versions |
|
|
54
68
|
| `dshm_upgrade` | Upgrade to the latest |
|
|
69
|
+
| `dshm_toggle` | Toggle a plugin on/off (0.4.0; confirm by default, execute directly when the user already asked in the same message) |
|
|
55
70
|
| `dshm_restart` | Restart DSH Web (with user consent) |
|
|
56
71
|
|
|
57
72
|
## CLI
|
|
@@ -59,9 +74,10 @@ After any mutation, the already-open Market and Installed views refresh the prof
|
|
|
59
74
|
```sh
|
|
60
75
|
dshm search [--query topic] [--category ui]
|
|
61
76
|
dshm list | outdated | registry
|
|
62
|
-
dshm install --id dsh-web-search
|
|
63
|
-
dshm upgrade --pkg dsh-web-search --yes
|
|
77
|
+
dshm install --id dsh-web-search [--force] # --force: skip the precheck gate after confirming the risk
|
|
78
|
+
dshm upgrade --pkg dsh-web-search --yes [--force]
|
|
64
79
|
dshm uninstall --pkg dsh-web-search --yes
|
|
80
|
+
dshm toggle --pkg dsh-web-search --on|--off --yes # file-level edit; applies on restart
|
|
65
81
|
dshm restart --yes
|
|
66
82
|
```
|
|
67
83
|
|
package/README.md
CHANGED
|
@@ -37,21 +37,36 @@
|
|
|
37
37
|
| 视图 | 能力 |
|
|
38
38
|
|---|---|
|
|
39
39
|
| **市场** | 收录卡片流;关键词搜索、分类筛选(服务端过滤 + 分页,每页 50 条,1,000 条清单也只探测当前页);卡片展开详情与「安装」;npm 源锁定最新精确版本,GitHub 源锁定 release/tag 指向的 commit |
|
|
40
|
-
| **已装** | web profile 实装列表,标注「市场安装 / 非市场安装」;可升级徽标、升级、两段式确认卸载;📖 README 预览(64KB
|
|
40
|
+
| **已装** | web profile 实装列表,标注「市场安装 / 非市场安装」;可升级徽标、升级、两段式确认卸载;📖 README 预览(64KB 截断);**运行相位徽标**(●active / ●failed / ○pending)与**一键开关**(0.4.0:委派官方 pluginManager 服务活体生效,服务缺席时文件级编辑 + 重启提示;dsh-m 自身与官方宿主命脉锁定不可开关) |
|
|
41
41
|
| **设置** | registry 地址草稿 +「校验并应用 / 恢复默认 / 下载默认 registry.json / 检查条目可达性」;配置地址、生效来源与状态一目了然;强制刷新;dsh-m 自更新 |
|
|
42
42
|
|
|
43
43
|
安装 / 卸载 / 升级完成后,当前已打开的市场页与已装页会一起重新读取 profile 状态并同步徽标/卡片,不需要关闭后重新打开插件市场;随后出现「⚡ 一键重启」横幅——受 systemd 管理时通过 DSH launcher 的 `appExit` 交给服务的 `Restart` 策略,避免在待停止 unit 的 cgroup 内启动 `systemctl` helper;无 `appExit` 的 systemd 兜底改用 manager-owned transient `systemd-run`,最后才退回 detached-helper。客户端按 boot id 确认新进程已恢复后关闭横幅,交由 DSH Web 自身的后台连接重试恢复页面,不强制整页刷新,避免认证/路由切换期间白屏。已完成当前 DSH Web `0.1.5-rc.1` 运行时的 live 核验:web profile 已加载 dsh-m `0.2.11`,重启后 `/dshm` ping 返回 `version: 0.2.11`,携带当前认证 token 的页面请求流程为 `303 → 200`;无认证请求会被拒绝。此次白屏截图对应的 404 在服务稳定后未复现;journal 显示截图时段发生多次 `status=75/TEMPFAIL` 重启,当前暂判定为重启/认证过渡窗口现象,未发现 dsh-m Host 路由崩溃。DSH `0.1.2-rc.1` live E2E、transient fallback live E2E,以及连续安装/卸载实验仍是正式发布前的验证 gap。安装过程实时显示 pnpm 进度(解析 → 下载 → 链接 → 构建)。
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
### 0.4.0 新增
|
|
46
|
+
|
|
47
|
+
- **开关(Enablement Toggle)**:已装卡片一键启停,内部自动路由行覆盖(单行插件,即时生效)或 Bundle 选择(多行插件);写路径委派官方 `pluginManager` 服务、缺席时降级 loader 直操作([ADR-0001](./docs/adr/0001-delegate-with-fallback-for-plugin-manager.md))。
|
|
48
|
+
- **运行相位徽标**:loader fiber 状态投影,failed 一眼可见。
|
|
49
|
+
- **保护名单**:`dsh-m` 自身与官方宿主命脉 16 项不可开关、不可卸载(升级不受影响)。
|
|
50
|
+
- **精确构建放行**:needs-builds 拦截后按 pnpm 待决名单逐键放行,全量放行降为兜底并如实标注([ADR-0002](./docs/adr/0002-precise-build-approval.md))。
|
|
51
|
+
- **peer 兼容预检**:安装/升级前校验 `@deepseek-ai/dsh(-*)` peers 与运行时版本(GitHub 源明示未检);不兼容时 GUI 弹确认、agent 回结构化结果、CLI `--force`。
|
|
52
|
+
- **实测版本清单(verified)**:registry 条目可选 `verified` 数组记录实测过的 DSH 运行时版本——实测声明而非预测声明,只展示不拦截。
|
|
53
|
+
- **bundle 身份验证**:装后检测无补丁层的包并警告「已装入为纯依赖」。
|
|
54
|
+
|
|
55
|
+
### 0.4.x 退役
|
|
56
|
+
|
|
57
|
+
- **元数据源竞速**:0.4.0 曾引入 npmjs / npmmirror ping 竞速选择元数据读取源,现整体移除(含 `probeEnabled` / `probeTimeoutMs` / `probeCacheTtlMin` 三个设置项与设置页展示)。官方同款探测只服务于「安装对话框 registry 默认预选」,dsh-m 无此交互;实测宿主机 npmjs 稳定更快,探测恒等默认行为。元数据读取固定走 npmjs,与安装链路(profile `.npmrc` 默认源)一致。
|
|
58
|
+
|
|
59
|
+
## Agent 工具(8 个)
|
|
46
60
|
|
|
47
61
|
| 工具 | 用途 |
|
|
48
62
|
|---|---|
|
|
49
63
|
| `dshm_search` | 搜收录清单(对话内出卡片) |
|
|
50
64
|
| `dshm_list` | 列已装插件(市场/非市场标注) |
|
|
51
|
-
| `dshm_install` | 按收录 id
|
|
65
|
+
| `dshm_install` | 按收录 id 安装(装前 peer 兼容预检;不兼容时回结构化结果,用户确认后 `force` 重试) |
|
|
52
66
|
| `dshm_uninstall` | 卸载(先确认;不删数据,报告残留路径) |
|
|
53
67
|
| `dshm_outdated` | 检查最新版本 |
|
|
54
68
|
| `dshm_upgrade` | 升级到最新 |
|
|
69
|
+
| `dshm_toggle` | 切换插件运行状态(0.4.0;默认先确认,用户同句已明确可直接执行) |
|
|
55
70
|
| `dshm_restart` | 一键重启 DSH Web(先征得同意) |
|
|
56
71
|
|
|
57
72
|
## CLI
|
|
@@ -59,9 +74,10 @@
|
|
|
59
74
|
```sh
|
|
60
75
|
dshm search [--query 主题] [--category ui]
|
|
61
76
|
dshm list | outdated | registry
|
|
62
|
-
dshm install --id dsh-web-search
|
|
63
|
-
dshm upgrade --pkg dsh-web-search --yes
|
|
77
|
+
dshm install --id dsh-web-search [--force] # --force:确认兼容风险后跳过预检拦截
|
|
78
|
+
dshm upgrade --pkg dsh-web-search --yes [--force]
|
|
64
79
|
dshm uninstall --pkg dsh-web-search --yes
|
|
80
|
+
dshm toggle --pkg dsh-web-search --on|--off --yes # 文件级编辑,重启生效
|
|
65
81
|
dshm restart --yes
|
|
66
82
|
```
|
|
67
83
|
|
package/docs/DESIGN.md
CHANGED
|
@@ -86,16 +86,26 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
|
|
|
86
86
|
- **卸载**(事务定序写死):validate(严格读取:NOT_INSTALLED / NOT_DSH_PLUGIN / PLUGIN_METADATA_UNREADABLE,全部零写入零快照)→ 快照 → live-disable(先让 client bundle 下线,避免 404;回滚时尽力反向)→ 摘除该包补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ remove → verify gone(仍在则回滚)。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
|
|
87
87
|
- **升级**:**按需检查**(`dshm_outdated` / `dshm_list` 时实时比对本地版本 vs npm latest / GitHub release,半自动——展示升级计划,确认后执行,即重开一次安装事务)。**不做后台定时器**。
|
|
88
88
|
- **自更新**:dsh-m 对自己同样做版本比对 + 提示升级(设置页呈现);执行即 `install-npm` 事务(integrity 缺失直接拒绝,不进事务)。
|
|
89
|
+
- **开关(0.4.0,术语见 CONTEXT.md「开关/行覆盖/Bundle 选择/委派降级」)**:已装插件运行状态的可逆切换,**不经 Profile 变更事务**。粒度自动路由——包内补丁层单 insert 行且无配置补丁行 → 行覆盖(`cordis.patch.yml` 保注释 `disabled` 编辑,官方 `writePluginEnabled` 同款语义,启用写显式 `disabled: false`);多行/不可解析 → Bundle 选择(`dsh.profile.bundles` 数组增删)。写路径委派优先:运行时探测官方 `pluginManager` 服务(`ctx.get`,按存在性不判版本号)→ `setPluginEnabled`/`setBundleEnabled`(白拿保护判定、unaddressable 识别、hmr 活体重组;applied 以复读 `listPlugins()` 为准);服务缺席降级 loader `entry.update()` 直操作 + 锁内文件编辑(bundle 级恒 restart-required)。**必须持官方同款锁**(`withFileLock(<profile>/package.json)`)防与官方 UI 并发丢更新。架构决策见 [ADR-0001](./adr/0001-delegate-with-fallback-for-plugin-manager.md)。
|
|
90
|
+
- **运行相位(0.4.0)**:loader fiber 状态投影(active/failed/pending/loading/unloading;fiber 缺失即已停用),读路径始终自读 loader entries,读路径零服务依赖。
|
|
91
|
+
- **保护名单(0.4.0)**:`dsh-m` 自身 + 逐字镜像官方 `protectedModules` 16 项全集(锚点 `dsh-plugin-manager@0.1.7-rc.2 lib/index.js L1077`;DSH 升级后 diff 官方名单)。作用于**开关与卸载**(保护门为开关路径最先判定,纯名单检查先于在装枚举);升级不受影响;委派路径信任官方 `readOnlyReason`。
|
|
92
|
+
- **peer 兼容预检(0.4.0)**:install/upgrade 事务外 metadata 阶段,校验 `@deepseek-ai/dsh(-*)` peers 与运行时版本(`workspace:^/~/ *` 视为运行时版本;semver includePrerelease;官方 `evaluatePluginCompatibility` 同语义)。运行时版本解析失败或预检元数据读取失败 → 不拦(如实标注未检);不兼容 → GUI 弹确认 / agent 回结构化 issue / CLI `--force`;无豁免机制(确认即通道);GitHub 源明示 `compatSkipped`。
|
|
93
|
+
- **精确构建放行(0.4.0,[ADR-0002](./adr/0002-precise-build-approval.md))**:needs-builds 拦截点(adapter 的 `makeAddViaLadder`)先读 `pnpm-workspace.yaml` 的 pnpm 待决名单(值为 `set this to true or false` 的无通配符键),逐键写 `allowBuilds.<pkg>: true`;名单空/不可读才走全量兜底并如实标注。放行结果沿 `RunnerOutcome.buildApprovals`/`fallbackAllBuilds` 上行(`usedAllowAllBuilds` 字段已删除)。
|
|
94
|
+
- **bundle 身份验证(0.4.0)**:事务 verify 阶段观察——包内无 `cordis.patch.yml` 且不在 bundles 数组 → `bundleWarning: 'no-patch-layer'`(装成纯依赖),只警告不回滚(存在合法的无补丁层 dsh 包)。
|
|
95
|
+
- **实测版本清单(0.4.0,术语「实测版本清单」)**:registry 条目可选 `verified: string[]`(精确 semver),来自各仓库对照源的实测声明;只展示与收录质量提示,**不做安装拦截依据**。
|
|
96
|
+
- **元数据源竞速(0.4.0 引入,0.4.x 退役)**:曾照搬官方 `dsh-client-ui-plugin-manager` 的 `PluginRegistryProbe`(npmjs/npmmirror `/-/ping` 竞速选元数据读取源)。退役理由:官方竞速只服务于「安装对话框 registry 默认预选」这一交互,dsh-m 无此交互;host 实测 npmjs(~175ms)稳定快于 npmmirror(~360-1400ms),探测恒等默认值;且元数据读取已有 TTL cache + deadline 预算。npmjs 真不可达时按官方 host 语义补**失败驱动 fallback**(对 network/timeout/not-found/no-matching-version 顺序重试镜像),不再引入主动探测。残留 `probeEnabled/probeTimeoutMs/probeCacheTtlMin` 配置键被非 strict Config schema 静默忽略,无需迁移。
|
|
89
97
|
- **重启**:内置**一键重启**。在服务管理器托管的 DSH 进程内,只有确认 unit 的 `Restart=on-failure` / `Restart=always` 且 `75` 未被 `SuccessExitStatus` / `RestartPreventExitStatus` 覆盖时,才调用 launcher 提供的 `appExit(75)`;策略未知或不满足时改用 manager-owned transient `systemd-run` 调用 `systemctl`,不猜测或硬编码 unit 名称,也不在即将停止的 cgroup 内 detached spawn `systemctl`。无 systemd/appExit 时再退回 detached-helper 兼容路径。安装/卸载/升级完成后 GUI 弹「需重启生效 [一键重启]」横幅;客户端以 boot id 确认替换进程后关闭横幅,交由 DSH Web 自身后台连接恢复,不强制整页刷新;工具返回重启提示。
|
|
90
98
|
- **安全基线(5 条)**:
|
|
91
99
|
1. 所有拉取仅 HTTPS + 响应大小上限 + 超时;
|
|
92
100
|
2. npm 安装校验 integrity;
|
|
93
101
|
3. GitHub 安装强制 pinned SHA;
|
|
94
|
-
4. pnpm
|
|
102
|
+
4. pnpm 构建脚本被拦时,先按 pnpm 待决名单**逐键精确放行**(ADR-0002),名单不可读才 `dangerouslyAllowAllBuilds` 全量兜底,且**必须明确报告**放行了哪些包/是否兜底;
|
|
95
103
|
5. 不做签名/验签体系(自用,明确不做)。
|
|
96
104
|
|
|
97
105
|
## 4. GUI(旗舰,v1 必须做好)
|
|
98
106
|
|
|
107
|
+
0.4.0 增补:已装卡右上 `dshm-switch` 开关(受 `toggleable` 控制,锁因 title 提示);sub 行相位点 `● active · v1.0.8 · npm`(相位点只映射 phase 五值,「已停用」归 Switch,两输入源各管各的);开关结果通知按 `applied` 分流(live → 绿 toast「即时生效」;restart-required → 沿用重启横幅 + 一键重启);安装遇兼容拦截(409 + issue)弹「仍要安装」确认(红字风险 + peers 清单 → `forceIncompatible` 重发)。
|
|
108
|
+
|
|
99
109
|
3 个视图,卡片展开式详情(不做独立详情页),**中文优先**,跟随 DSH Web 深色主题:
|
|
100
110
|
|
|
101
111
|
1. **市场页**(默认):registry 卡片流;搜索/分类为**服务端过滤**(Host 强制 `withLatest=true`、每页 50、1,000 条清单第一页只探测当前页);`MarketPanel` 是市场/已装数据唯一 owner(请求 generation + AbortController 丢弃旧响应);安装/卸载/升级完成后通过统一协调器同时刷新市场与已装快照,不要求关闭并重新打开面板;分页控件 + 超过 200 条性能提示 + 默认/自定义/缓存/不可用短提示(不含本地路径);卡片详情保留 README markdown 预览与 npm/GitHub 官方外链。
|
package/lib/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* registry/search/list/outdated 在清单不可用时打印配置/实际生效地址并 exit 1。
|
|
7
7
|
*/
|
|
8
8
|
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, } from './core/market.js';
|
|
9
|
+
import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
|
|
9
10
|
import { loadRegistry } from './core/registry.js';
|
|
10
11
|
import { scheduleRestart } from './core/restart.js';
|
|
11
12
|
import { pathToFileURL } from 'node:url';
|
|
@@ -43,6 +44,16 @@ function needFlag(flags, name) {
|
|
|
43
44
|
}
|
|
44
45
|
return v.trim();
|
|
45
46
|
}
|
|
47
|
+
/** 构建放行输出(ADR-0002):列包名;兜底全量放行如实标注。 */
|
|
48
|
+
function outBuildsNote(res, out) {
|
|
49
|
+
if (res.fallbackAllBuilds === true) {
|
|
50
|
+
out('⚠️ 该插件执行了构建脚本(名单不可读,已全量兜底放行)。');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const names = res.buildApprovals ?? [];
|
|
54
|
+
if (names.length > 0)
|
|
55
|
+
out(`⚠️ 该插件执行了构建脚本,已精确放行:${names.join('、')}。`);
|
|
56
|
+
}
|
|
46
57
|
function requireYes(flags, action) {
|
|
47
58
|
if (flags.yes !== true) {
|
|
48
59
|
throw new Error(`拒绝执行:${action} 是变更操作,必须带 --yes 显式确认。`);
|
|
@@ -88,9 +99,10 @@ const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
|
|
|
88
99
|
dshm registry 查看收录清单来源与条目
|
|
89
100
|
|
|
90
101
|
变更命令(必须 --yes):
|
|
91
|
-
dshm install --id <收录id> [--version 1.2.3]
|
|
92
|
-
dshm upgrade --pkg <包名> --yes
|
|
102
|
+
dshm install --id <收录id> [--version 1.2.3] [--force] (--force:确认兼容风险后跳过预检拦截)
|
|
103
|
+
dshm upgrade --pkg <包名> --yes [--force]
|
|
93
104
|
dshm uninstall --pkg <包名> --yes
|
|
105
|
+
dshm toggle --pkg <包名> --on|--off --yes (切换运行状态;文件级编辑,重启生效)
|
|
94
106
|
dshm restart --yes
|
|
95
107
|
注意:变更互斥仅在进程内生效——变更执行期间不要同时从 GUI / Agent 工具发起另一次变更。
|
|
96
108
|
|
|
@@ -117,6 +129,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
117
129
|
installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
|
|
118
130
|
uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
|
|
119
131
|
upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
|
|
132
|
+
togglePlugin: deps.togglePlugin ?? coreTogglePlugin,
|
|
120
133
|
};
|
|
121
134
|
const { cmd, flags } = parseArgs(argv);
|
|
122
135
|
const cfg = cliConfig();
|
|
@@ -215,25 +228,45 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
215
228
|
case 'install': {
|
|
216
229
|
const id = needFlag(flags, 'id');
|
|
217
230
|
const version = typeof flags.version === 'string' ? flags.version : undefined;
|
|
218
|
-
const res = await d.installFromRegistry(id, cfg, {
|
|
231
|
+
const res = await d.installFromRegistry(id, cfg, {
|
|
232
|
+
version,
|
|
233
|
+
forceIncompatible: flags.force === true,
|
|
234
|
+
namespace: 'cli',
|
|
235
|
+
});
|
|
219
236
|
out(`✅ 已安装 ${res.pkg}(${res.spec})`);
|
|
220
|
-
|
|
221
|
-
out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
|
|
237
|
+
outBuildsNote(res, out);
|
|
222
238
|
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
223
239
|
return 0;
|
|
224
240
|
}
|
|
225
241
|
case 'upgrade': {
|
|
226
242
|
const target = needFlag(flags, 'pkg');
|
|
227
243
|
requireYes(flags, '升级');
|
|
228
|
-
const res = await d.upgradePlugin(target, cfg, {
|
|
244
|
+
const res = await d.upgradePlugin(target, cfg, {
|
|
245
|
+
forceIncompatible: flags.force === true,
|
|
246
|
+
namespace: 'cli',
|
|
247
|
+
});
|
|
229
248
|
const from = res.fromVersion ? `v${res.fromVersion} → ` : '';
|
|
230
249
|
const to = res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : '最新';
|
|
231
250
|
out(`✅ 已升级 ${res.pkg}(${from}${to})`);
|
|
232
|
-
|
|
233
|
-
out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
|
|
251
|
+
outBuildsNote(res, out);
|
|
234
252
|
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
235
253
|
return 0;
|
|
236
254
|
}
|
|
255
|
+
case 'toggle': {
|
|
256
|
+
const target = needFlag(flags, 'pkg');
|
|
257
|
+
const onFlag = flags.on === true;
|
|
258
|
+
const offFlag = flags.off === true;
|
|
259
|
+
if (onFlag === offFlag)
|
|
260
|
+
throw new Error('开关命令需要恰好一个 --on 或 --off');
|
|
261
|
+
requireYes(flags, '切换插件运行状态');
|
|
262
|
+
const res = await d.togglePlugin(target, onFlag);
|
|
263
|
+
out(`✅ 已${onFlag ? '启用' : '停用'} ${res.pkg}(profile 文件已更新)`);
|
|
264
|
+
if (res.applied === 'live')
|
|
265
|
+
out('本次进程内已即时生效;重启后状态保持。');
|
|
266
|
+
else
|
|
267
|
+
out('需要重启 DSH Web 生效:dshm restart --yes');
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
237
270
|
case 'uninstall': {
|
|
238
271
|
const target = needFlag(flags, 'pkg');
|
|
239
272
|
requireYes(flags, '卸载');
|
package/lib/client.js
CHANGED
|
@@ -306,7 +306,10 @@ function installedViewModel(it) {
|
|
|
306
306
|
sourceLabelKey: { npm: "src.npm", github: "src.github", link: "src.link", file: "src.file", unknown: "src.unknown" }[it.source] || null,
|
|
307
307
|
guard: uninstallGuardKeys(it),
|
|
308
308
|
latestLabel: it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : ""),
|
|
309
|
-
latestLabelDetail: it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : "\u2014")
|
|
309
|
+
latestLabelDetail: it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : "\u2014"),
|
|
310
|
+
// 0.4.0(Task 17):运行相位/开关状态的 sub 行渲染原料;相位点 class 映射归 toggle-view
|
|
311
|
+
phaseKey: it.phase ? `phase.${it.phase}` : null,
|
|
312
|
+
enabledLabelKey: it.enabled === false ? "toggle.state.off" : "toggle.state.on"
|
|
310
313
|
};
|
|
311
314
|
}
|
|
312
315
|
function registrySourceKey(data) {
|
|
@@ -334,6 +337,51 @@ var init_installed_view = __esm({
|
|
|
334
337
|
}
|
|
335
338
|
});
|
|
336
339
|
|
|
340
|
+
// src/client/toggle-view.js
|
|
341
|
+
var toggle_view_exports = {};
|
|
342
|
+
__export(toggle_view_exports, {
|
|
343
|
+
toggleNoticeKeys: () => toggleNoticeKeys,
|
|
344
|
+
toggleViewModel: () => toggleViewModel
|
|
345
|
+
});
|
|
346
|
+
function toggleViewModel(it) {
|
|
347
|
+
const phase = PHASE_VIEW[it.phase] ?? { phaseDotClass: "", phaseLabelKey: null };
|
|
348
|
+
const toggleable = it.toggleable !== false && it.lockReason === void 0;
|
|
349
|
+
return {
|
|
350
|
+
phaseDotClass: phase.phaseDotClass,
|
|
351
|
+
phaseLabelKey: phase.phaseLabelKey,
|
|
352
|
+
switchOn: it.enabled === true,
|
|
353
|
+
switchDisabled: !toggleable,
|
|
354
|
+
switchTitleKey: it.lockReason ? LOCK_TITLE_KEYS[it.lockReason] || null : null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function toggleNoticeKeys(result) {
|
|
358
|
+
const direction = result.enabled === true ? "on" : "off";
|
|
359
|
+
const applied = result.applied === "live" ? "live" : "restart";
|
|
360
|
+
return {
|
|
361
|
+
textKey: `notify.toggled.${direction}.${applied}`,
|
|
362
|
+
params: { pkg: result.pkg || "" },
|
|
363
|
+
needsRestart: applied === "restart"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
var PHASE_VIEW, LOCK_TITLE_KEYS;
|
|
367
|
+
var init_toggle_view = __esm({
|
|
368
|
+
"src/client/toggle-view.js"() {
|
|
369
|
+
"use strict";
|
|
370
|
+
PHASE_VIEW = {
|
|
371
|
+
active: { phaseDotClass: "ok", phaseLabelKey: "phase.active" },
|
|
372
|
+
failed: { phaseDotClass: "err", phaseLabelKey: "phase.failed" },
|
|
373
|
+
pending: { phaseDotClass: "idle", phaseLabelKey: "phase.pending" },
|
|
374
|
+
loading: { phaseDotClass: "busy", phaseLabelKey: "phase.loading" },
|
|
375
|
+
unloading: { phaseDotClass: "busy", phaseLabelKey: "phase.unloading" }
|
|
376
|
+
};
|
|
377
|
+
LOCK_TITLE_KEYS = {
|
|
378
|
+
self: "toggle.lock.self",
|
|
379
|
+
protected: "toggle.lock.protected",
|
|
380
|
+
"no-entry": "toggle.lock.noentry"
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
337
385
|
// src/client/tool-view.js
|
|
338
386
|
var tool_view_exports = {};
|
|
339
387
|
__export(tool_view_exports, {
|
|
@@ -438,6 +486,7 @@ var { MARKET_PAGE_SIZE: MARKET_PAGE_SIZE2, normalizeMarketQuery: normalizeMarket
|
|
|
438
486
|
var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
|
|
439
487
|
var { ExtLink, MdImg, renderMarkdown } = createMarkdown2(h);
|
|
440
488
|
var { installedViewModel: installedViewModel2, registrySourceKey: registrySourceKey2 } = (init_installed_view(), __toCommonJS(installed_view_exports));
|
|
489
|
+
var { toggleViewModel: toggleViewModel2, toggleNoticeKeys: toggleNoticeKeys2 } = (init_toggle_view(), __toCommonJS(toggle_view_exports));
|
|
441
490
|
var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_view(), __toCommonJS(tool_view_exports));
|
|
442
491
|
var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_MS2, nextRestartWait: nextRestartWait2, isAmbiguousRestartRequestError: isAmbiguousRestartRequestError2 } = (init_restart_wait(), __toCommonJS(restart_wait_exports));
|
|
443
492
|
var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
|
|
@@ -572,6 +621,29 @@ var ZH = {
|
|
|
572
621
|
"registry.refreshed": "\u6536\u5F55\u6E05\u5355\u5DF2\u5F3A\u5236\u5237\u65B0",
|
|
573
622
|
"notify.installed": "\u5DF2\u5B89\u88C5 {pkg}{version}",
|
|
574
623
|
"notify.allowbuilds": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF0C\u5DF2\u6309\u7B56\u7565\u653E\u884C\uFF09",
|
|
624
|
+
"notify.builds": "\uFF08\u5DF2\u7CBE\u786E\u653E\u884C\u6784\u5EFA\u811A\u672C\uFF1A{names}\uFF09",
|
|
625
|
+
"notify.builds.fallback": "\uFF08\u6CE8\u610F\uFF1A\u6784\u5EFA\u811A\u672C\u540D\u5355\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u5168\u91CF\u515C\u5E95\u653E\u884C\uFF09",
|
|
626
|
+
"notify.bundlewarning": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u5305\u65E0\u8865\u4E01\u5C42\uFF0C\u5DF2\u88C5\u5165\u4E3A\u7EAF\u4F9D\u8D56\u4E0D\u4F1A\u751F\u6548\uFF1B\u53EF\u5378\u8F7D\u6216\u5230\u6536\u5F55\u4ED3\u5E93\u53CD\u9988\uFF09",
|
|
627
|
+
"notify.toggled.on.live": "\u5DF2\u542F\u7528 {pkg}\uFF08\u5373\u65F6\u751F\u6548\uFF09",
|
|
628
|
+
"notify.toggled.off.live": "\u5DF2\u505C\u7528 {pkg}\uFF08\u5373\u65F6\u751F\u6548\uFF09",
|
|
629
|
+
"notify.toggled.on.restart": "\u5DF2\u542F\u7528 {pkg}\uFF08\u9700\u91CD\u542F\u751F\u6548\uFF09",
|
|
630
|
+
"notify.toggled.off.restart": "\u5DF2\u505C\u7528 {pkg}\uFF08\u9700\u91CD\u542F\u751F\u6548\uFF09",
|
|
631
|
+
"toggle.state.on": "\u8FD0\u884C\u4E2D",
|
|
632
|
+
"toggle.state.off": "\u5DF2\u505C\u7528",
|
|
633
|
+
"toggle.lock.self": "dsh-m \u81EA\u8EAB\u4E0D\u53EF\u5F00\u5173",
|
|
634
|
+
"toggle.lock.protected": "\u5B98\u65B9\u5BBF\u4E3B\u547D\u8109\uFF0C\u4E0D\u53EF\u5F00\u5173",
|
|
635
|
+
"toggle.lock.noentry": "\u672A\u88C5\u8F7D\uFF08\u4E0D\u5728\u52A0\u8F7D\u6811\uFF09",
|
|
636
|
+
"toggle.failed": "\u5F00\u5173\u64CD\u4F5C\u5931\u8D25\uFF1A{err}",
|
|
637
|
+
"phase.active": "active",
|
|
638
|
+
"phase.failed": "failed",
|
|
639
|
+
"phase.pending": "pending",
|
|
640
|
+
"phase.loading": "loading",
|
|
641
|
+
"phase.unloading": "unloading",
|
|
642
|
+
"compat.title": "\u517C\u5BB9\u6027\u98CE\u9669\u786E\u8BA4",
|
|
643
|
+
"compat.body": "{pkg}@{version} \u58F0\u660E\u7684 peerDependencies \u4E0E\u5F53\u524D DSH {runtime} \u4E0D\u517C\u5BB9\uFF1A",
|
|
644
|
+
"compat.risk": "\u7EE7\u7EED\u5B89\u88C5\u53EF\u80FD\u5BFC\u81F4\u5D29\u6E83\u6216\u6570\u636E\u4E22\u5931\u3002\u786E\u5B9A\u4ECD\u8981\u5B89\u88C5\u5417\uFF1F",
|
|
645
|
+
"compat.force": "\u4ECD\u8981\u5B89\u88C5",
|
|
646
|
+
"common.cancel": "\u53D6\u6D88",
|
|
575
647
|
"notify.uninstalled": "\u5DF2\u5378\u8F7D {pkg}",
|
|
576
648
|
"notify.livedisabled": "\uFF08\u5DF2\u5148\u4E0B\u7EBF\u8FD0\u884C\u4E2D\u7684\u754C\u9762\uFF09",
|
|
577
649
|
"notify.leftovers": "\uFF1B\u68C0\u6D4B\u5230\u7591\u4F3C\u6B8B\u7559\u6570\u636E\uFF1A{paths}",
|
|
@@ -740,6 +812,29 @@ var EN = {
|
|
|
740
812
|
"registry.refreshed": "Registry force-refreshed",
|
|
741
813
|
"notify.installed": "Installed {pkg}{version}",
|
|
742
814
|
"notify.allowbuilds": " (note: this plugin ran build scripts, allowed by policy)",
|
|
815
|
+
"notify.builds": " (build scripts precisely allowed: {names})",
|
|
816
|
+
"notify.builds.fallback": " (note: pending list unreadable; all builds allowed as fallback)",
|
|
817
|
+
"notify.bundlewarning": " (note: no patch layer \u2014 installed as a plain dependency; uninstall or report to the listing repo)",
|
|
818
|
+
"notify.toggled.on.live": "Enabled {pkg} (applied live)",
|
|
819
|
+
"notify.toggled.off.live": "Disabled {pkg} (applied live)",
|
|
820
|
+
"notify.toggled.on.restart": "Enabled {pkg} (restart required)",
|
|
821
|
+
"notify.toggled.off.restart": "Disabled {pkg} (restart required)",
|
|
822
|
+
"toggle.state.on": "running",
|
|
823
|
+
"toggle.state.off": "disabled",
|
|
824
|
+
"toggle.lock.self": "dsh-m itself cannot be toggled",
|
|
825
|
+
"toggle.lock.protected": "host lifeline module; not toggleable",
|
|
826
|
+
"toggle.lock.noentry": "not composed (absent from loader tree)",
|
|
827
|
+
"toggle.failed": "Toggle failed: {err}",
|
|
828
|
+
"phase.active": "active",
|
|
829
|
+
"phase.failed": "failed",
|
|
830
|
+
"phase.pending": "pending",
|
|
831
|
+
"phase.loading": "loading",
|
|
832
|
+
"phase.unloading": "unloading",
|
|
833
|
+
"compat.title": "Compatibility risk",
|
|
834
|
+
"compat.body": "{pkg}@{version} declares peerDependencies incompatible with DSH {runtime}:",
|
|
835
|
+
"compat.risk": "Proceeding may cause crashes or data loss. Install anyway?",
|
|
836
|
+
"compat.force": "Install anyway",
|
|
837
|
+
"common.cancel": "Cancel",
|
|
743
838
|
"notify.uninstalled": "Uninstalled {pkg}",
|
|
744
839
|
"notify.livedisabled": " (live UI disabled first)",
|
|
745
840
|
"notify.leftovers": "; possible leftover data: {paths}",
|
|
@@ -897,6 +992,26 @@ var CSS = `
|
|
|
897
992
|
.dshm-dshchip:hover .dshm-dshchip-tip,.dshm-dshchip:focus-visible .dshm-dshchip-tip{visibility:visible;opacity:1}
|
|
898
993
|
.dshm-dshchip-tiprow{font:12px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}
|
|
899
994
|
.dshm-dshchip-tipsub{display:block;margin-top:2px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-caption,#9ca3af)}
|
|
995
|
+
|
|
996
|
+
/* 0.4.0\uFF1A\u5F00\u5173 / \u76F8\u4F4D\u70B9 / \u517C\u5BB9\u786E\u8BA4\u5F39\u7A97 */
|
|
997
|
+
.dshm-dot{font-size:9px;line-height:1;vertical-align:middle;margin-right:2px}
|
|
998
|
+
.dshm-dot.ok{color:#22c55e}
|
|
999
|
+
.dshm-dot.err{color:#ef4444}
|
|
1000
|
+
.dshm-dot.idle{color:#9ca3af}
|
|
1001
|
+
.dshm-dot.busy{color:#f59e0b}
|
|
1002
|
+
.dshm-switch{position:relative;display:inline-flex;align-items:center;width:34px;height:19px;border-radius:10px;background:var(--dsw-alias-fill-secondary,#d1d5db);cursor:pointer;transition:background .15s;flex:none;margin-left:auto}
|
|
1003
|
+
.dshm-switch input{display:none}
|
|
1004
|
+
.dshm-switch.on{background:#22c55e}
|
|
1005
|
+
.dshm-switch.locked{opacity:.45;cursor:not-allowed}
|
|
1006
|
+
.dshm-switch-slider{position:absolute;left:2px;top:2px;width:15px;height:15px;border-radius:50%;background:#fff;transition:left .15s;box-shadow:0 1px 2px rgba(0,0,0,.25)}
|
|
1007
|
+
.dshm-switch.on .dshm-switch-slider{left:17px}
|
|
1008
|
+
.dshm-compat-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:60}
|
|
1009
|
+
.dshm-compat-dialog{max-width:420px;width:calc(100% - 32px);background:var(--dsw-alias-bg-layer-2,#1c2230);border:1px solid var(--dsw-alias-border-l2,#333b4d);border-radius:12px;padding:16px}
|
|
1010
|
+
.dshm-compat-title{font-weight:600;margin-bottom:8px;color:#f87171}
|
|
1011
|
+
.dshm-compat-body{font-size:12px;margin-bottom:6px}
|
|
1012
|
+
.dshm-compat-peers{margin:0 0 8px;padding-left:18px;font-size:12px}
|
|
1013
|
+
.dshm-compat-peers li{font-family:ui-monospace,monospace}
|
|
1014
|
+
.dshm-compat-risk{font-size:12px;color:#f87171;margin-bottom:12px}
|
|
900
1015
|
`;
|
|
901
1016
|
function ensureCss() {
|
|
902
1017
|
if (typeof document === "undefined" || document.getElementById("dshm-css")) return;
|
|
@@ -913,7 +1028,11 @@ async function api(method, params, signal) {
|
|
|
913
1028
|
...signal ? { signal } : {}
|
|
914
1029
|
});
|
|
915
1030
|
const data = await res.json().catch(() => ({}));
|
|
916
|
-
if (!res.ok || data.ok === false)
|
|
1031
|
+
if (!res.ok || data.ok === false) {
|
|
1032
|
+
const err = new Error(data.error || `API ${res.status}`);
|
|
1033
|
+
if (data && typeof data === "object" && data.issue) err.issue = data.issue;
|
|
1034
|
+
throw err;
|
|
1035
|
+
}
|
|
917
1036
|
return data;
|
|
918
1037
|
}
|
|
919
1038
|
function fmtDate(iso) {
|
|
@@ -1012,6 +1131,29 @@ function LinksRow(props) {
|
|
|
1012
1131
|
});
|
|
1013
1132
|
return h("div", { className: "dshm-links" }, h("span", { className: "dshm-links-k" }, `${lookup("detail.links")}\uFF1A`), ...kids);
|
|
1014
1133
|
}
|
|
1134
|
+
function PhaseDot({ vm }) {
|
|
1135
|
+
if (!vm.phaseDotClass) return null;
|
|
1136
|
+
return h(
|
|
1137
|
+
"span",
|
|
1138
|
+
{ className: `dshm-dot ${vm.phaseDotClass}`, title: vm.phaseLabelKey ? lookup(vm.phaseLabelKey) : null },
|
|
1139
|
+
"\u25CF"
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
function ToggleSwitch({ vm, disabled, onChange, title }) {
|
|
1143
|
+
return h(
|
|
1144
|
+
"label",
|
|
1145
|
+
{
|
|
1146
|
+
className: `dshm-switch${vm.switchOn ? " on" : ""}${vm.switchDisabled ? " locked" : ""}`,
|
|
1147
|
+
title: title || (vm.switchTitleKey ? lookup(vm.switchTitleKey) : null),
|
|
1148
|
+
onClick: (e) => {
|
|
1149
|
+
e.stopPropagation();
|
|
1150
|
+
if (!vm.switchDisabled && !disabled) onChange(!vm.switchOn);
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
h("input", { type: "checkbox", checked: vm.switchOn, disabled: vm.switchDisabled || disabled, readOnly: true }),
|
|
1154
|
+
h("span", { className: "dshm-switch-slider" })
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1015
1157
|
function TwoStepButton({ label, confirmLabel, className, onConfirm, disabled }) {
|
|
1016
1158
|
const [arm, setArm] = useState(false);
|
|
1017
1159
|
useEffect(() => {
|
|
@@ -1105,25 +1247,70 @@ function MarketTab({ notify, market, onMutation }) {
|
|
|
1105
1247
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
1106
1248
|
debounceRef.current = setTimeout(() => updateQuery({ query: value }), 300);
|
|
1107
1249
|
};
|
|
1108
|
-
const
|
|
1250
|
+
const [compatConfirm, setCompatConfirm] = useState(null);
|
|
1251
|
+
const installDone = (res) => {
|
|
1252
|
+
notify({
|
|
1253
|
+
kind: "ok",
|
|
1254
|
+
needsRestart: true,
|
|
1255
|
+
text: lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (res.bundleWarning === "no-patch-layer" ? lookup("notify.bundlewarning") : "")
|
|
1256
|
+
});
|
|
1257
|
+
};
|
|
1258
|
+
const doInstall = async (it, version, forceIncompatible) => {
|
|
1109
1259
|
setBusyId(it.id);
|
|
1110
1260
|
try {
|
|
1111
|
-
const res = await api("install", { id: it.id, ...version ? { version } : {} });
|
|
1112
|
-
|
|
1113
|
-
kind: "ok",
|
|
1114
|
-
needsRestart: true,
|
|
1115
|
-
text: lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + (res.usedAllowAllBuilds ? lookup("notify.allowbuilds") : "")
|
|
1116
|
-
});
|
|
1261
|
+
const res = await api("install", { id: it.id, ...version ? { version } : {}, ...forceIncompatible ? { forceIncompatible: true } : {} });
|
|
1262
|
+
installDone(res);
|
|
1117
1263
|
await (onMutation ? onMutation() : reload(false));
|
|
1118
1264
|
} catch (e) {
|
|
1119
|
-
|
|
1265
|
+
if (e && e.issue) {
|
|
1266
|
+
setCompatConfirm({ it, version, issue: e.issue });
|
|
1267
|
+
} else {
|
|
1268
|
+
notify({ kind: "err", text: lookup("failed.install", { err: e && e.message || e }) });
|
|
1269
|
+
}
|
|
1120
1270
|
} finally {
|
|
1121
1271
|
setBusyId(null);
|
|
1122
1272
|
}
|
|
1123
1273
|
};
|
|
1274
|
+
const CompatDialog = compatConfirm ? h(
|
|
1275
|
+
"div",
|
|
1276
|
+
{ className: "dshm-compat-overlay", onClick: () => setCompatConfirm(null) },
|
|
1277
|
+
h(
|
|
1278
|
+
"div",
|
|
1279
|
+
{ className: "dshm-compat-dialog", onClick: (e) => e.stopPropagation() },
|
|
1280
|
+
h("div", { className: "dshm-compat-title" }, lookup("compat.title")),
|
|
1281
|
+
h("div", { className: "dshm-compat-body" }, lookup("compat.body", {
|
|
1282
|
+
pkg: compatConfirm.issue.pkg || compatConfirm.it.pkg,
|
|
1283
|
+
version: compatConfirm.issue.version || "?",
|
|
1284
|
+
runtime: compatConfirm.issue.runtimeVersion || "?"
|
|
1285
|
+
})),
|
|
1286
|
+
h(
|
|
1287
|
+
"ul",
|
|
1288
|
+
{ className: "dshm-compat-peers" },
|
|
1289
|
+
...Object.entries(compatConfirm.issue.peers || {}).map(([name, range]) => h("li", { key: name }, `${name}: ${range}`))
|
|
1290
|
+
),
|
|
1291
|
+
h("div", { className: "dshm-compat-risk" }, lookup("compat.risk")),
|
|
1292
|
+
h(
|
|
1293
|
+
"div",
|
|
1294
|
+
{ className: "dshm-actions" },
|
|
1295
|
+
h("button", {
|
|
1296
|
+
className: "dshm-btn sm",
|
|
1297
|
+
onClick: () => setCompatConfirm(null)
|
|
1298
|
+
}, lookup("common.cancel")),
|
|
1299
|
+
h("button", {
|
|
1300
|
+
className: "dshm-btn primary sm",
|
|
1301
|
+
onClick: () => {
|
|
1302
|
+
const pending = compatConfirm;
|
|
1303
|
+
setCompatConfirm(null);
|
|
1304
|
+
doInstall(pending.it, pending.version, true);
|
|
1305
|
+
}
|
|
1306
|
+
}, lookup("compat.force"))
|
|
1307
|
+
)
|
|
1308
|
+
)
|
|
1309
|
+
) : null;
|
|
1124
1310
|
return h(
|
|
1125
1311
|
React.Fragment,
|
|
1126
1312
|
null,
|
|
1313
|
+
CompatDialog,
|
|
1127
1314
|
notice ? h(
|
|
1128
1315
|
"div",
|
|
1129
1316
|
{ className: notice.key === "notice.unavailable" ? "dshm-err" : "dshm-hint" },
|
|
@@ -1277,6 +1464,27 @@ function InstalledTab({ notify, installed, onMutation }) {
|
|
|
1277
1464
|
const [openPkg, setOpenPkg] = useState(null);
|
|
1278
1465
|
const [readmePkg, setReadmePkg] = useState(null);
|
|
1279
1466
|
const [busyPkg, setBusyPkg] = useState(null);
|
|
1467
|
+
const doToggle = async (it, enabled) => {
|
|
1468
|
+
setBusyPkg(it.pkg);
|
|
1469
|
+
const call = () => api("set-enabled", { pkg: it.pkg, enabled });
|
|
1470
|
+
try {
|
|
1471
|
+
let res;
|
|
1472
|
+
try {
|
|
1473
|
+
res = await call();
|
|
1474
|
+
} catch (first) {
|
|
1475
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1476
|
+
res = await call();
|
|
1477
|
+
}
|
|
1478
|
+
const note = toggleNoticeKeys2(res);
|
|
1479
|
+
const extra = res.warnings && res.warnings.length ? `\uFF08${res.warnings.join("\uFF1B")}\uFF09` : "";
|
|
1480
|
+
notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
|
|
1481
|
+
await (onMutation ? onMutation() : reload());
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
|
|
1484
|
+
} finally {
|
|
1485
|
+
setBusyPkg(null);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1280
1488
|
const doUninstall = async (it) => {
|
|
1281
1489
|
setBusyPkg(it.pkg);
|
|
1282
1490
|
try {
|
|
@@ -1304,7 +1512,7 @@ function InstalledTab({ notify, installed, onMutation }) {
|
|
|
1304
1512
|
pkg: res.pkg,
|
|
1305
1513
|
from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
|
|
1306
1514
|
to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
|
|
1307
|
-
}) + (res.
|
|
1515
|
+
}) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "")
|
|
1308
1516
|
});
|
|
1309
1517
|
await (onMutation ? onMutation() : reload());
|
|
1310
1518
|
} catch (e) {
|
|
@@ -1328,19 +1536,27 @@ function InstalledTab({ notify, installed, onMutation }) {
|
|
|
1328
1536
|
{ className: "dshm-cards" },
|
|
1329
1537
|
items.map((it) => {
|
|
1330
1538
|
const vm = installedViewModel2(it);
|
|
1539
|
+
const tvm = toggleViewModel2(it);
|
|
1331
1540
|
return Card({
|
|
1332
1541
|
key: it.pkg,
|
|
1333
1542
|
icon: h(Icon, { entry: { name: it.name, github: vm.githubRepo, icon: null } }),
|
|
1334
1543
|
name: it.name,
|
|
1544
|
+
topRight: h(ToggleSwitch, {
|
|
1545
|
+
vm: tvm,
|
|
1546
|
+
disabled: busyPkg === it.pkg,
|
|
1547
|
+
onChange: (enabled) => doToggle(it, enabled)
|
|
1548
|
+
}),
|
|
1335
1549
|
badges: [
|
|
1336
1550
|
it.outdated ? h("span", { className: "dshm-badge warn", key: "u" }, `\u2B06 ${vm.latestLabel}`.trim()) : null,
|
|
1337
1551
|
it.registryId ? h("span", { className: "dshm-badge", key: "r" }, lookup("badge.market")) : h("span", { className: "dshm-badge info", key: "r" }, lookup("badge.nonmarket"))
|
|
1338
1552
|
],
|
|
1339
1553
|
desc: it.description || "\uFF08\u65E0\u63CF\u8FF0\uFF09",
|
|
1340
1554
|
sub: [
|
|
1555
|
+
h(PhaseDot, { key: "dot", vm: tvm }),
|
|
1556
|
+
vm.phaseKey ? lookup(vm.phaseKey) : lookup(vm.enabledLabelKey),
|
|
1341
1557
|
`v${it.version || "?"}`,
|
|
1342
1558
|
vm.sourceLabelKey ? lookup(vm.sourceLabelKey) : it.source
|
|
1343
|
-
].
|
|
1559
|
+
].map((part, i) => i === 0 ? part : [" \xB7 ", part]).flat(),
|
|
1344
1560
|
links: h(LinksRow, {
|
|
1345
1561
|
npm: it.source === "npm" ? it.pkg : null,
|
|
1346
1562
|
github: vm.githubRepo
|
|
@@ -1628,7 +1844,7 @@ function DetailRows(rows) {
|
|
|
1628
1844
|
])
|
|
1629
1845
|
);
|
|
1630
1846
|
}
|
|
1631
|
-
function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, actions }) {
|
|
1847
|
+
function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, actions, topRight }) {
|
|
1632
1848
|
return h(
|
|
1633
1849
|
"div",
|
|
1634
1850
|
{
|
|
@@ -1644,7 +1860,7 @@ function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, ac
|
|
|
1644
1860
|
h(
|
|
1645
1861
|
"div",
|
|
1646
1862
|
{ className: "dshm-meta" },
|
|
1647
|
-
h("div", { className: "dshm-top" }, h("span", { className: "dshm-name" }, name), ...badges.filter(Boolean)),
|
|
1863
|
+
h("div", { className: "dshm-top" }, h("span", { className: "dshm-name" }, name), ...badges.filter(Boolean), topRight || null),
|
|
1648
1864
|
h("div", { className: "dshm-desc", style: open ? { WebkitLineClamp: "unset" } : null }, desc),
|
|
1649
1865
|
sub ? h("div", { className: "dshm-sub" }, sub) : null,
|
|
1650
1866
|
links || null,
|