dshmarket 1.11.2 → 1.11.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 +3 -2
- package/README.zh.md +6 -2
- package/client/client.js +119 -119
- package/lib/hot.js +8 -2
- package/lib/patch.js +7 -19
- package/lib/profile.js +32 -16
- package/lib/routes.js +42 -3
- package/lib/types/profile.d.ts +19 -0
- package/lib/verify.js +24 -2
- package/package.json +2 -2
- package/src/hot.ts +8 -2
- package/src/patch.ts +7 -15
- package/src/profile.ts +34 -16
- package/src/routes.ts +42 -3
- package/src/verify.ts +24 -2
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Restart `dsh web`, then open **Settings → Plugin Market**.
|
|
|
27
27
|
|
|
28
28
|
## What you get
|
|
29
29
|
|
|
30
|
-
- **Browse & search** the full community catalog (
|
|
30
|
+
- **Browse & search** the full community catalog (1200+ plugins, growing daily) — category filters, star counts, top/new sorting, bilingual descriptions that follow your UI language
|
|
31
31
|
- **Screenshots** — AppStore-style screenshots in the install dialog: author-curated via the registry, with automatic README extraction as fallback; images load from GitHub hosting only, and only after you open the dialog
|
|
32
32
|
- **Themes** — a dedicated tab for community themes and skins: install → active immediately, switch with one click (themes are mutually exclusive, your choice survives restarts), uninstall to revert
|
|
33
33
|
- **One-click install** — confirm the source, watch live progress; most plugins go live after a page refresh, no restart
|
|
@@ -37,9 +37,10 @@ Restart `dsh web`, then open **Settings → Plugin Market**.
|
|
|
37
37
|
- **Hot disable / enable** — toggles write `- id: …` + `disabled: true|false` into the profile's `cordis.patch.yml` (the official patch layer, mechanism ported from [dsh-plugin-hub](https://github.com/Noob-stupid/dsh-plugin-hub)): DSH's HMR re-composes within ~1s, no restart, and the loader re-applies the choice on every boot; hand-edited patch rows show as badges, host-infrastructure plugins are protected from toggling, and a malformed patch file is never made worse
|
|
38
38
|
- **Restart when needed** — changes that cannot hot-load show a one-click restart beside the pending-change banner; the action is restricted to same-origin loopback requests
|
|
39
39
|
- **Zero jargon** — if a component is missing (pnpm), the market detects it and offers a one-click automatic setup
|
|
40
|
-
- **Log export** — one click produces a sanitized plain-text log for bug reports (home paths and credential shapes are masked; nothing is ever sent anywhere)
|
|
40
|
+
- **Log export** — one click produces a sanitized plain-text log for bug reports (home paths and credential shapes are masked; nothing is ever sent anywhere). The market's version sits next to the page heading, so a screenshot of a problem already carries it
|
|
41
41
|
- **Diagnostics** — the plugin load order and conflict surface, one page: bundle stack with official/community badges, duplicate loader entries, dependency version mismatches, multi-version core packages, overrides and invalid config entries. Plain-language terms, problem blocks highlighted, everything collapsible
|
|
42
42
|
|
|
43
|
+
- **Load order** — drag community bundles into the order you want, or take the suggested one derived from the plugins' own before/after rules. Nothing is written until a trial composition passes, and the panel tells you what the new order would change (overrides, invalid or duplicate entries) before you apply it
|
|
43
44
|
- **AI fix** — one click copies a diagnostics-driven fix prompt (errors/warnings/order conflicts + conservative scope instructions) to the clipboard; you paste it into a new conversation and decide whether to send
|
|
44
45
|
|
|
45
46
|
## Speed
|
package/README.zh.md
CHANGED
|
@@ -27,16 +27,20 @@ dsh plugin --profile web add dshmarket
|
|
|
27
27
|
|
|
28
28
|
## 你会得到
|
|
29
29
|
|
|
30
|
-
- **逛与搜**——完整社区目录(
|
|
30
|
+
- **逛与搜**——完整社区目录(1200+ 插件,每天在涨),分类筛选、star 数、最热/最新排序,中英描述跟随界面语言
|
|
31
31
|
- **截图展示**——安装弹窗内 App Store 式截图:作者可在 registry 里策展,没有则自动从 README 抽取;图片仅从 GitHub 图床加载,且只在你打开弹窗后才发请求
|
|
32
32
|
- **主题**——独立主题页:装完立即生效,点一下切换(主题互斥、选择跨重启保留),卸载即恢复
|
|
33
33
|
- **一键安装**——确认来源,实时进度;多数插件刷新页面即可用,无需重启
|
|
34
34
|
- **备份与恢复**——把 profile 的插件清单与配置导出为可读 JSON,换机导入,存到 WebDAV 并每日自动备份,或通过私有 GitHub Gist 跨机器同步;恢复采用**合并**方式(备份之后新装的插件会保留),写入前校验、失败自动回滚
|
|
35
35
|
- **更新**——逐插件检测(npm 版本或锁定 commit 对比 HEAD),一键更新或全部更新;市场自己也走同一通道升级
|
|
36
36
|
- **卸载**——两步确认防误触;本次会话装的插件即点即卸
|
|
37
|
+
- **热禁用 / 启用**——开关会往 profile 的 `cordis.patch.yml`(官方补丁层,机制移植自 [dsh-plugin-hub](https://github.com/Noob-stupid/dsh-plugin-hub))写入 `- id: …` + `disabled: true|false`:DSH 的 HMR 约 1 秒内重新组合,无需重启,loader 每次启动都会重新应用这个选择;手工改过的补丁行会显示成徽标,宿主基础设施插件禁止开关,补丁文件格式不对时绝不会被写得更糟
|
|
37
38
|
- **按需重启**——无法热加载的变更会在待重启提示旁显示一键重启;操作仅接受本机同源请求
|
|
38
39
|
- **零术语**——缺组件(pnpm)时市场自己发现、一键自动装好,全程不见命令行
|
|
39
|
-
- **导出日志**——一键生成脱敏纯文本日志方便反馈(home
|
|
40
|
+
- **导出日志**——一键生成脱敏纯文本日志方便反馈(home 路径与密钥形状已打码;任何数据都不会被上传)。市场版本号就在标题旁边,截图反馈时自带版本信息
|
|
41
|
+
- **诊断**——插件加载顺序与冲突一页看全:bundle 栈(官方/社区徽标)、重复的 loader 条目、依赖版本不一致、核心包多版本共存、覆盖项与非法配置条目。术语说人话,问题块高亮,全部可折叠
|
|
42
|
+
- **加载顺序**——拖拽调整社区 bundle 的顺序,或直接采用按插件自身 before/after 规则推导出的建议顺序。写入前先跑一次静态组合校验,通过才落盘;应用之前面板会告诉你这个顺序会改变什么(多少处覆盖、多少无效或重复条目)
|
|
43
|
+
- **AI 修复**——一键把诊断结果生成的修复 prompt(错误/警告/顺序冲突 + 保守的改动范围约束)复制到剪贴板,你粘进新对话,自己决定发不发
|
|
40
44
|
|
|
41
45
|
## 速度
|
|
42
46
|
|
package/client/client.js
CHANGED
|
@@ -919,146 +919,146 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
919
919
|
document.head.appendChild(tag);
|
|
920
920
|
}
|
|
921
921
|
var Market_module_css_default = {
|
|
922
|
-
"
|
|
923
|
-
"
|
|
924
|
-
"
|
|
925
|
-
"
|
|
926
|
-
"
|
|
927
|
-
"
|
|
928
|
-
"
|
|
929
|
-
"barFill": "eGUBIq_barFill",
|
|
930
|
-
"tabSearch": "eGUBIq_tabSearch",
|
|
931
|
-
"switchOn": "eGUBIq_switchOn",
|
|
932
|
-
"modalNote": "eGUBIq_modalNote",
|
|
933
|
-
"diagVal": "eGUBIq_diagVal",
|
|
934
|
-
"assignRow": "eGUBIq_assignRow",
|
|
935
|
-
"collapseHead": "eGUBIq_collapseHead",
|
|
922
|
+
"on": "eGUBIq_on",
|
|
923
|
+
"diagBadgeOfficial": "eGUBIq_diagBadgeOfficial",
|
|
924
|
+
"shots": "eGUBIq_shots",
|
|
925
|
+
"tab": "eGUBIq_tab",
|
|
926
|
+
"backupCard": "eGUBIq_backupCard",
|
|
927
|
+
"spin": "eGUBIq_spin",
|
|
928
|
+
"ovByTag": "eGUBIq_ovByTag",
|
|
936
929
|
"spec": "eGUBIq_spec",
|
|
937
|
-
"
|
|
938
|
-
"
|
|
930
|
+
"inlineInput": "eGUBIq_inlineInput",
|
|
931
|
+
"star": "eGUBIq_star",
|
|
932
|
+
"backupWarn": "eGUBIq_backupWarn",
|
|
933
|
+
"specTagGit": "eGUBIq_specTagGit",
|
|
934
|
+
"groupHead": "eGUBIq_groupHead",
|
|
935
|
+
"collapseIcon": "eGUBIq_collapseIcon",
|
|
939
936
|
"card": "eGUBIq_card",
|
|
940
|
-
"backupActions": "eGUBIq_backupActions",
|
|
941
|
-
"diagBadgeShadow": "eGUBIq_diagBadgeShadow",
|
|
942
937
|
"dragging": "eGUBIq_dragging",
|
|
943
|
-
"
|
|
944
|
-
"
|
|
945
|
-
"
|
|
946
|
-
"
|
|
947
|
-
"
|
|
948
|
-
"
|
|
949
|
-
"
|
|
950
|
-
"
|
|
951
|
-
"
|
|
952
|
-
"root": "eGUBIq_root",
|
|
953
|
-
"groupName": "eGUBIq_groupName",
|
|
938
|
+
"tabSearchRow": "eGUBIq_tabSearchRow",
|
|
939
|
+
"head": "eGUBIq_head",
|
|
940
|
+
"barWave": "eGUBIq_barWave",
|
|
941
|
+
"owner": "eGUBIq_owner",
|
|
942
|
+
"groupMember": "eGUBIq_groupMember",
|
|
943
|
+
"cmd": "eGUBIq_cmd",
|
|
944
|
+
"bannerIcon": "eGUBIq_bannerIcon",
|
|
945
|
+
"swatches": "eGUBIq_swatches",
|
|
946
|
+
"ovFrom": "eGUBIq_ovFrom",
|
|
954
947
|
"sub": "eGUBIq_sub",
|
|
955
|
-
"
|
|
956
|
-
"
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"diagArrow": "eGUBIq_diagArrow",
|
|
961
|
-
"viewBar": "eGUBIq_viewBar",
|
|
948
|
+
"dragHandle": "eGUBIq_dragHandle",
|
|
949
|
+
"diagPage": "eGUBIq_diagPage",
|
|
950
|
+
"titleRow": "eGUBIq_titleRow",
|
|
951
|
+
"tabs": "eGUBIq_tabs",
|
|
952
|
+
"diagRow": "eGUBIq_diagRow",
|
|
962
953
|
"sectionOverview": "eGUBIq_sectionOverview",
|
|
954
|
+
"specTag": "eGUBIq_specTag",
|
|
955
|
+
"switch": "eGUBIq_switch",
|
|
956
|
+
"collapseBody": "eGUBIq_collapseBody",
|
|
963
957
|
"orphBadge": "eGUBIq_orphBadge",
|
|
964
|
-
"
|
|
965
|
-
"
|
|
966
|
-
"
|
|
967
|
-
"
|
|
968
|
-
"sectAction": "eGUBIq_sectAction",
|
|
969
|
-
"diagEmpty": "eGUBIq_diagEmpty",
|
|
970
|
-
"swatches": "eGUBIq_swatches",
|
|
971
|
-
"dragHandle": "eGUBIq_dragHandle",
|
|
972
|
-
"viewOn": "eGUBIq_viewOn",
|
|
973
|
-
"cmd": "eGUBIq_cmd",
|
|
958
|
+
"backupMessage": "eGUBIq_backupMessage",
|
|
959
|
+
"diagBadgeShadow": "eGUBIq_diagBadgeShadow",
|
|
960
|
+
"bar": "eGUBIq_bar",
|
|
961
|
+
"backupCheckList": "eGUBIq_backupCheckList",
|
|
974
962
|
"groupAddPanel": "eGUBIq_groupAddPanel",
|
|
975
|
-
"
|
|
976
|
-
"
|
|
977
|
-
"ovByTag": "eGUBIq_ovByTag",
|
|
978
|
-
"diagKey": "eGUBIq_diagKey",
|
|
979
|
-
"tab": "eGUBIq_tab",
|
|
980
|
-
"backupGrid": "eGUBIq_backupGrid",
|
|
981
|
-
"cats": "eGUBIq_cats",
|
|
963
|
+
"descTight": "eGUBIq_descTight",
|
|
964
|
+
"av": "eGUBIq_av",
|
|
982
965
|
"catsRow": "eGUBIq_catsRow",
|
|
983
|
-
"actWhy": "eGUBIq_actWhy",
|
|
984
|
-
"deprecate": "eGUBIq_deprecate",
|
|
985
|
-
"nm": "eGUBIq_nm",
|
|
986
|
-
"groupHint": "eGUBIq_groupHint",
|
|
987
|
-
"diagRow": "eGUBIq_diagRow",
|
|
988
|
-
"switch": "eGUBIq_switch",
|
|
989
|
-
"groupHead": "eGUBIq_groupHead",
|
|
990
|
-
"dot": "eGUBIq_dot",
|
|
991
|
-
"head": "eGUBIq_head",
|
|
992
|
-
"top": "eGUBIq_top",
|
|
993
|
-
"hiddenFile": "eGUBIq_hiddenFile",
|
|
994
|
-
"backupInput": "eGUBIq_backupInput",
|
|
995
|
-
"grid": "eGUBIq_grid",
|
|
996
|
-
"irowMissing": "eGUBIq_irowMissing",
|
|
997
966
|
"okState": "eGUBIq_okState",
|
|
998
|
-
"
|
|
999
|
-
"
|
|
1000
|
-
"diagSection": "eGUBIq_diagSection",
|
|
1001
|
-
"err": "eGUBIq_err",
|
|
1002
|
-
"tabSearchRow": "eGUBIq_tabSearchRow",
|
|
967
|
+
"grow": "eGUBIq_grow",
|
|
968
|
+
"pager": "eGUBIq_pager",
|
|
1003
969
|
"depBadge": "eGUBIq_depBadge",
|
|
1004
|
-
"
|
|
1005
|
-
"
|
|
1006
|
-
"
|
|
1007
|
-
"
|
|
1008
|
-
"
|
|
970
|
+
"groupRow": "eGUBIq_groupRow",
|
|
971
|
+
"act": "eGUBIq_act",
|
|
972
|
+
"version": "eGUBIq_version",
|
|
973
|
+
"viewBar": "eGUBIq_viewBar",
|
|
974
|
+
"switchKnob": "eGUBIq_switchKnob",
|
|
975
|
+
"groupName": "eGUBIq_groupName",
|
|
976
|
+
"diagBundle": "eGUBIq_diagBundle",
|
|
977
|
+
"panelNote": "eGUBIq_panelNote",
|
|
978
|
+
"tag": "eGUBIq_tag",
|
|
979
|
+
"diagSummaryItem": "eGUBIq_diagSummaryItem",
|
|
1009
980
|
"body": "eGUBIq_body",
|
|
1010
|
-
"
|
|
1011
|
-
"
|
|
1012
|
-
"fixFallback": "eGUBIq_fixFallback",
|
|
1013
|
-
"themesGrid": "eGUBIq_themesGrid",
|
|
1014
|
-
"dragOver": "eGUBIq_dragOver",
|
|
1015
|
-
"topBtn": "eGUBIq_topBtn",
|
|
1016
|
-
"diagSummary": "eGUBIq_diagSummary",
|
|
1017
|
-
"loading": "eGUBIq_loading",
|
|
1018
|
-
"descTight": "eGUBIq_descTight",
|
|
981
|
+
"progress": "eGUBIq_progress",
|
|
982
|
+
"catsCollapsed": "eGUBIq_catsCollapsed",
|
|
1019
983
|
"row1": "eGUBIq_row1",
|
|
984
|
+
"dot": "eGUBIq_dot",
|
|
985
|
+
"ovRow": "eGUBIq_ovRow",
|
|
986
|
+
"specTagFile": "eGUBIq_specTagFile",
|
|
987
|
+
"root": "eGUBIq_root",
|
|
988
|
+
"cats": "eGUBIq_cats",
|
|
989
|
+
"pct": "eGUBIq_pct",
|
|
1020
990
|
"desc": "eGUBIq_desc",
|
|
1021
|
-
"
|
|
1022
|
-
"
|
|
1023
|
-
"staleAction": "eGUBIq_staleAction",
|
|
991
|
+
"switchOn": "eGUBIq_switchOn",
|
|
992
|
+
"backupInput": "eGUBIq_backupInput",
|
|
1024
993
|
"actLive": "eGUBIq_actLive",
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
994
|
+
"assignRow": "eGUBIq_assignRow",
|
|
995
|
+
"groupMembers": "eGUBIq_groupMembers",
|
|
996
|
+
"groupCreate": "eGUBIq_groupCreate",
|
|
1027
997
|
"shot": "eGUBIq_shot",
|
|
1028
|
-
"
|
|
998
|
+
"diagSummaryMeta": "eGUBIq_diagSummaryMeta",
|
|
1029
999
|
"diagMeta": "eGUBIq_diagMeta",
|
|
1030
|
-
"
|
|
1000
|
+
"viewOn": "eGUBIq_viewOn",
|
|
1001
|
+
"ovArrow": "eGUBIq_ovArrow",
|
|
1002
|
+
"diagVal": "eGUBIq_diagVal",
|
|
1003
|
+
"staleAction": "eGUBIq_staleAction",
|
|
1004
|
+
"collapseHead": "eGUBIq_collapseHead",
|
|
1005
|
+
"dshmSlide": "eGUBIq_dshmSlide",
|
|
1006
|
+
"bannerHint": "eGUBIq_bannerHint",
|
|
1007
|
+
"dragOver": "eGUBIq_dragOver",
|
|
1008
|
+
"diagIndex": "eGUBIq_diagIndex",
|
|
1009
|
+
"actWarn": "eGUBIq_actWarn",
|
|
1010
|
+
"diagAlert": "eGUBIq_diagAlert",
|
|
1011
|
+
"backupGrid": "eGUBIq_backupGrid",
|
|
1012
|
+
"empty": "eGUBIq_empty",
|
|
1013
|
+
"diagBadgeCommunity": "eGUBIq_diagBadgeCommunity",
|
|
1014
|
+
"top": "eGUBIq_top",
|
|
1015
|
+
"fixFallback": "eGUBIq_fixFallback",
|
|
1016
|
+
"diagList": "eGUBIq_diagList",
|
|
1017
|
+
"srcBtn": "eGUBIq_srcBtn",
|
|
1018
|
+
"tabSearch": "eGUBIq_tabSearch",
|
|
1019
|
+
"orphRow": "eGUBIq_orphRow",
|
|
1020
|
+
"diagSummary": "eGUBIq_diagSummary",
|
|
1031
1021
|
"fixFallbackText": "eGUBIq_fixFallbackText",
|
|
1032
|
-
"
|
|
1022
|
+
"diagEmpty": "eGUBIq_diagEmpty",
|
|
1023
|
+
"modalNote": "eGUBIq_modalNote",
|
|
1024
|
+
"grid": "eGUBIq_grid",
|
|
1025
|
+
"nm": "eGUBIq_nm",
|
|
1026
|
+
"depLine": "eGUBIq_depLine",
|
|
1027
|
+
"switchMixed": "eGUBIq_switchMixed",
|
|
1033
1028
|
"src": "eGUBIq_src",
|
|
1034
|
-
"
|
|
1029
|
+
"irowMissing": "eGUBIq_irowMissing",
|
|
1030
|
+
"catsToggle": "eGUBIq_catsToggle",
|
|
1031
|
+
"barFill": "eGUBIq_barFill",
|
|
1032
|
+
"assignSelect": "eGUBIq_assignSelect",
|
|
1033
|
+
"collapseTitle": "eGUBIq_collapseTitle",
|
|
1034
|
+
"groupHint": "eGUBIq_groupHint",
|
|
1035
|
+
"actWhy": "eGUBIq_actWhy",
|
|
1036
|
+
"loading": "eGUBIq_loading",
|
|
1037
|
+
"backupCheck": "eGUBIq_backupCheck",
|
|
1038
|
+
"diagSection": "eGUBIq_diagSection",
|
|
1039
|
+
"topBtn": "eGUBIq_topBtn",
|
|
1040
|
+
"banner": "eGUBIq_banner",
|
|
1041
|
+
"catsWrap": "eGUBIq_catsWrap",
|
|
1042
|
+
"themesGrid": "eGUBIq_themesGrid",
|
|
1043
|
+
"irow": "eGUBIq_irow",
|
|
1044
|
+
"groupActions": "eGUBIq_groupActions",
|
|
1035
1045
|
"actBroken": "eGUBIq_actBroken",
|
|
1036
|
-
"
|
|
1037
|
-
"
|
|
1038
|
-
"
|
|
1039
|
-
"
|
|
1040
|
-
"
|
|
1041
|
-
"
|
|
1042
|
-
"
|
|
1046
|
+
"title": "eGUBIq_title",
|
|
1047
|
+
"backupActions": "eGUBIq_backupActions",
|
|
1048
|
+
"hiddenFile": "eGUBIq_hiddenFile",
|
|
1049
|
+
"pageEllipsis": "eGUBIq_pageEllipsis",
|
|
1050
|
+
"sectAction": "eGUBIq_sectAction",
|
|
1051
|
+
"deprecate": "eGUBIq_deprecate",
|
|
1052
|
+
"diagArrow": "eGUBIq_diagArrow",
|
|
1053
|
+
"sp": "eGUBIq_sp",
|
|
1054
|
+
"diagCount": "eGUBIq_diagCount",
|
|
1043
1055
|
"viewBtn": "eGUBIq_viewBtn",
|
|
1044
|
-
"
|
|
1045
|
-
"
|
|
1056
|
+
"diagKey": "eGUBIq_diagKey",
|
|
1057
|
+
"err": "eGUBIq_err",
|
|
1046
1058
|
"pagerPages": "eGUBIq_pagerPages",
|
|
1047
|
-
"
|
|
1048
|
-
"
|
|
1049
|
-
"
|
|
1050
|
-
"bar": "eGUBIq_bar",
|
|
1051
|
-
"collapseBody": "eGUBIq_collapseBody",
|
|
1052
|
-
"diagSummaryMeta": "eGUBIq_diagSummaryMeta",
|
|
1053
|
-
"title": "eGUBIq_title",
|
|
1054
|
-
"titleRow": "eGUBIq_titleRow",
|
|
1055
|
-
"diagSummaryItem": "eGUBIq_diagSummaryItem",
|
|
1056
|
-
"backupCheck": "eGUBIq_backupCheck",
|
|
1057
|
-
"specTagFile": "eGUBIq_specTagFile",
|
|
1058
|
-
"groupMember": "eGUBIq_groupMember",
|
|
1059
|
-
"catsToggle": "eGUBIq_catsToggle",
|
|
1060
|
-
"version": "eGUBIq_version",
|
|
1061
|
-
"bannerIcon": "eGUBIq_bannerIcon"
|
|
1059
|
+
"warnLine": "eGUBIq_warnLine",
|
|
1060
|
+
"pageInfo": "eGUBIq_pageInfo",
|
|
1061
|
+
"foot": "eGUBIq_foot"
|
|
1062
1062
|
};
|
|
1063
1063
|
//#endregion
|
|
1064
1064
|
//#region src/client/Diagnostics.tsx
|
package/lib/hot.js
CHANGED
|
@@ -93,7 +93,13 @@ function readPkgDsh(profileDir, packageName) {
|
|
|
93
93
|
export function parseSimplePatch(patchText) {
|
|
94
94
|
const rows = [];
|
|
95
95
|
let pending = null;
|
|
96
|
-
|
|
96
|
+
// Split on CRLF too. `#.*$` cannot strip a comment that ends in \r —
|
|
97
|
+
// JS treats \r as a line terminator, so `.` will not cross it and `$`
|
|
98
|
+
// only anchors at the very end — leaving the comment text in place,
|
|
99
|
+
// matching none of the row shapes, and failing the whole patch. Any
|
|
100
|
+
// plugin whose patch was authored on Windows then reads as
|
|
101
|
+
// "contains config/expression rows" and can never hot-mount.
|
|
102
|
+
for (const raw of patchText.split(/\r?\n/)) {
|
|
97
103
|
const line = raw.replace(/#.*$/, '').trimEnd();
|
|
98
104
|
if (line.trim() === '')
|
|
99
105
|
continue;
|
|
@@ -386,7 +392,7 @@ export function readUserPatchControls(profileDir) {
|
|
|
386
392
|
const names = new Set();
|
|
387
393
|
try {
|
|
388
394
|
const text = readFileSync(join(profileDir, 'cordis.patch.yml'), 'utf8');
|
|
389
|
-
for (const line of text.split(
|
|
395
|
+
for (const line of text.split(/\r?\n/)) {
|
|
390
396
|
const id = /^\s*-?\s*id:\s*['"]?([A-Za-z0-9._/@-]+)/.exec(line);
|
|
391
397
|
if (id !== null)
|
|
392
398
|
ids.add(id[1]);
|
package/lib/patch.js
CHANGED
|
@@ -29,7 +29,7 @@ import { join } from 'node:path';
|
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
import { logEvent } from './log.js';
|
|
31
31
|
import { parsePatchFile } from './check.js';
|
|
32
|
-
import { bundlePatchInsertedIds } from './profile.js';
|
|
32
|
+
import { bundlePatchInsertedIds, parsePatchRows } from './profile.js';
|
|
33
33
|
/**
|
|
34
34
|
* Host infrastructure rows: disabling any of these breaks the very chain
|
|
35
35
|
* the patch layer runs on (e.g. timer → HMR, webserver → the page itself),
|
|
@@ -189,25 +189,13 @@ export function rowIdsForPackage(host, profileDirectory, packageName) {
|
|
|
189
189
|
catch { /* package not installed — loader side may still know it */ }
|
|
190
190
|
// The conventional location too: a package may ship cordis.patch.yml at
|
|
191
191
|
// its root without declaring dsh.bundle.patch (the loader probes it too).
|
|
192
|
-
// Same
|
|
192
|
+
// Same parser, deliberately: this used to be a second hand-rolled scan
|
|
193
|
+
// that closed the insert block only on `id:` lines, so `- disable:` with
|
|
194
|
+
// nested ids under it claimed the neighbour's rows — #147 all over again
|
|
195
|
+
// on the path #147 did not touch.
|
|
193
196
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const line = raw.replace(/#.*$/u, '');
|
|
197
|
-
if (line.trim() === '')
|
|
198
|
-
continue;
|
|
199
|
-
const indent = line.length - line.trimStart().length;
|
|
200
|
-
if (/^\s*-?\s*insert:\s*$/u.test(line)) {
|
|
201
|
-
insertIndent = indent;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const id = /^\s*-?\s*id:\s*['"]?([A-Za-z0-9_.-]+)/u.exec(line);
|
|
205
|
-
if (id === null)
|
|
206
|
-
continue;
|
|
207
|
-
if (insertIndent !== null && indent > insertIndent)
|
|
208
|
-
ids.add(id[1]);
|
|
209
|
-
else
|
|
210
|
-
insertIndent = null;
|
|
197
|
+
for (const id of parsePatchRows(readFileSync(join(packageDir, 'cordis.patch.yml'), 'utf8')).insertedIds) {
|
|
198
|
+
ids.add(id);
|
|
211
199
|
}
|
|
212
200
|
}
|
|
213
201
|
catch { /* no conventional patch — nothing more to add */ }
|
package/lib/profile.js
CHANGED
|
@@ -205,27 +205,30 @@ export function bundlePatchInsertedIds(dir) {
|
|
|
205
205
|
* but for "what does this bundle bring in" any row counts. `insertedIds` is
|
|
206
206
|
* the subset nested under an `insert:` key (#147).
|
|
207
207
|
*/
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
catch {
|
|
218
|
-
return { names: [], ids: [], insertedIds: [] };
|
|
219
|
-
}
|
|
208
|
+
/**
|
|
209
|
+
* Rows of one patch file. Exported because a package may ship its patch at
|
|
210
|
+
* the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
|
|
211
|
+
* patch layer has to read that one by the same rules — a second hand-rolled
|
|
212
|
+
* scan drifted from this one and re-introduced #147 on that path (it closed
|
|
213
|
+
* the insert block only on `id:` lines, so `- disable:` followed by nested
|
|
214
|
+
* ids claimed the neighbour's rows).
|
|
215
|
+
*/
|
|
216
|
+
export function parsePatchRows(text) {
|
|
220
217
|
const names = [];
|
|
221
218
|
const ids = [];
|
|
222
219
|
const insertedIds = [];
|
|
223
|
-
|
|
220
|
+
{
|
|
224
221
|
// `insert:` opens a nested list; every id below it, at deeper
|
|
225
222
|
// indentation, is a row this package brings in. A row at or above the
|
|
226
223
|
// `insert:` indentation closes the block — those target OTHER plugins.
|
|
227
224
|
let insertIndent = null;
|
|
228
|
-
for
|
|
225
|
+
// CRLF too, for consistency with the hot-mount parser, which a
|
|
226
|
+
// Windows-authored patch genuinely broke. Here it changes no outcome
|
|
227
|
+
// today — every row pattern below is `^`-anchored and a comment line
|
|
228
|
+
// always starts with `#`, so an unstripped comment matches nothing —
|
|
229
|
+
// and it is deliberately NOT covered by a spec, because a test that
|
|
230
|
+
// passes with or without the change tests nothing.
|
|
231
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
229
232
|
const line = raw.replace(/#.*$/, '');
|
|
230
233
|
if (line.trim() === '')
|
|
231
234
|
continue;
|
|
@@ -256,9 +259,22 @@ function readBundlePatchRows(dir) {
|
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
|
-
catch { /* unreadable patch — nothing to report */ }
|
|
260
262
|
return { names, ids, insertedIds };
|
|
261
263
|
}
|
|
264
|
+
/** Rows of the patch a package DECLARES through `dsh.bundle.patch`. */
|
|
265
|
+
function readBundlePatchRows(dir) {
|
|
266
|
+
const empty = { names: [], ids: [], insertedIds: [] };
|
|
267
|
+
try {
|
|
268
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
269
|
+
const declared = manifest.dsh?.bundle?.patch;
|
|
270
|
+
if (typeof declared !== 'string' || declared === '')
|
|
271
|
+
return empty;
|
|
272
|
+
return parsePatchRows(readFileSync(join(dir, declared), 'utf8'));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return empty;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
262
278
|
/** The profile manifest's `dsh.profile.bundles` — what the CLI reconciled. */
|
|
263
279
|
export function readProfileBundles(profileDirectory) {
|
|
264
280
|
try {
|
|
@@ -397,7 +413,7 @@ export function setAllowBuilds(profile, packages, explicitDir) {
|
|
|
397
413
|
const map = {};
|
|
398
414
|
const blockMatch = blockRe.exec(yaml);
|
|
399
415
|
if (blockMatch !== null) {
|
|
400
|
-
for (const line of blockMatch[1].split(
|
|
416
|
+
for (const line of blockMatch[1].split(/\r?\n/)) {
|
|
401
417
|
// The key itself may contain colons: git-hosted deps are only matched
|
|
402
418
|
// by a `name@git+https://…` key (#68). The anchored boolean tail makes
|
|
403
419
|
// the split land on the LAST colon, never inside a `://` — and doubles
|
package/lib/routes.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createGroup, deleteGroup, removeFromGroups, renameGroup, setGroupMember
|
|
|
15
15
|
import { exportLogs, logEvent } from './log.js';
|
|
16
16
|
import { diagnosePackageManifests } from './diagnostics.js';
|
|
17
17
|
import { BOOT_ID, cancelActive, probePnpm, progress, provisionPnpm, runDshPlugin, } from './dsh-cli.js';
|
|
18
|
-
import { INBOX_BUNDLES, profileDir, readInstalled, readInstalledManifest, readInstalledVersion, readLockCommits, readManifestDeps, readProfileBundles, restoreManifestDeps, setAllowBuilds } from './profile.js';
|
|
18
|
+
import { hasLoadableEntry, INBOX_BUNDLES, profileDir, readInstalled, readInstalledManifest, readInstalledVersion, readLockCommits, readManifestDeps, readProfileBundles, restoreManifestDeps, setAllowBuilds } from './profile.js';
|
|
19
19
|
import { analyzeProfile } from './check.js';
|
|
20
20
|
import { applyBundleOrder, mergeOrder, readBundleRules, readBundleStack, validateOrder } from './order.js';
|
|
21
21
|
import { trialValidate } from './trial.js';
|
|
@@ -238,8 +238,24 @@ export function mountMarketRoutes(host, config, commandRuntime) {
|
|
|
238
238
|
function liveNames() {
|
|
239
239
|
const live = new Set(listHotMounts());
|
|
240
240
|
for (const entry of host.loader.entries()) {
|
|
241
|
-
if (entry.fiber
|
|
241
|
+
if (entry.fiber === undefined)
|
|
242
|
+
continue;
|
|
243
|
+
if (entry.options.name !== undefined)
|
|
242
244
|
live.add(entry.options.name);
|
|
245
|
+
// Entry IDS too, under a `#` prefix that cannot collide with a package
|
|
246
|
+
// name. A CARRIER bundle's row names the package it mounts, not
|
|
247
|
+
// itself (#156: @tt-a1i/archify-dsh inserts an entry named
|
|
248
|
+
// @deepseek-ai/dsh-skill-filesystem), so its own name never appears
|
|
249
|
+
// here — but the id it created does, and that id is unique to its
|
|
250
|
+
// patch. Verification needs both, and putting them in one set means
|
|
251
|
+
// no call site can pass the names and forget the ids.
|
|
252
|
+
if (entry.options.id !== undefined && entry.options.id !== '') {
|
|
253
|
+
live.add(`#${entry.options.id}`);
|
|
254
|
+
// Loader ids may carry an include prefix (`include:archify-…`).
|
|
255
|
+
const bare = entry.options.id.split(':').pop();
|
|
256
|
+
if (bare !== undefined && bare !== entry.options.id)
|
|
257
|
+
live.add(`#${bare}`);
|
|
258
|
+
}
|
|
243
259
|
}
|
|
244
260
|
return live;
|
|
245
261
|
}
|
|
@@ -1056,6 +1072,24 @@ export function mountMarketRoutes(host, config, commandRuntime) {
|
|
|
1056
1072
|
if (stale)
|
|
1057
1073
|
ok = false;
|
|
1058
1074
|
}
|
|
1075
|
+
// The new build has to be loadable (#159). pnpm exits 0 for any
|
|
1076
|
+
// tarball it can extract, and the version really did change, so
|
|
1077
|
+
// nothing above notices a package that arrived without its entry
|
|
1078
|
+
// artifact — a registry mirror serving a source-only tarball for
|
|
1079
|
+
// a just-published version is the reported case, a plugin author
|
|
1080
|
+
// shipping a broken `files` list is the other one.
|
|
1081
|
+
//
|
|
1082
|
+
// Activation cannot stand in for this check: a package updating
|
|
1083
|
+
// ITSELF still reports live, because the running fiber belongs to
|
|
1084
|
+
// the OLD code that is already in memory. The failure only
|
|
1085
|
+
// surfaces on the next boot, as a profile that will not start.
|
|
1086
|
+
let brokenEntry = false;
|
|
1087
|
+
if (ok && !hasLoadableEntry(activeProfileDir, name)) {
|
|
1088
|
+
brokenEntry = true;
|
|
1089
|
+
ok = false;
|
|
1090
|
+
const rolledBack = restoreManifestDeps(config.profile, manifestBefore, activeProfileDir);
|
|
1091
|
+
logEvent('error', 'update', `${name}: updated build has no loadable entry — rolled back the pin${rolledBack.length > 0 ? ` (${rolledBack.join(', ')})` : ''}`);
|
|
1092
|
+
}
|
|
1059
1093
|
if (ok) {
|
|
1060
1094
|
invalidateUpdates();
|
|
1061
1095
|
activation = { [name]: verifyActivation(config.profile, name, liveNames(), activeProfileDir, disabled.has(name)) };
|
|
@@ -1071,6 +1105,11 @@ export function mountMarketRoutes(host, config, commandRuntime) {
|
|
|
1071
1105
|
: staleReason === 'release-age'
|
|
1072
1106
|
? '这个新版本刚发布不久。为了安全,系统默认会等它发布满一天后再安装——刚发布的版本偶尔会被发现问题然后撤回。可以明天再试,或点「立即更新」不再等待。 / This version was just released; for safety, installs normally wait about a day after a release. Try again tomorrow, or click "Update now" to install it right away.'
|
|
1073
1107
|
: '更新命令执行完成,但版本没有变化,原因未能确认。点「立即更新」重试通常能解决;若仍不行,请导出日志反馈。 / The update command completed but the version did not change; the cause could not be confirmed. Clicking "Update now" to retry usually resolves it — if not, export the log and report it.';
|
|
1108
|
+
// Actionable, because the user's own recovery is the right one:
|
|
1109
|
+
// the bad artifact is cached under its integrity hash, so a plain
|
|
1110
|
+
// re-add reuses it — the package has to be removed first.
|
|
1111
|
+
const brokenEntryError = !brokenEntry ? null
|
|
1112
|
+
: `${name} 更新后缺少入口文件(package.json 的 main/exports 指向的文件不存在),已回滚版本号以免下次启动失败。这通常是镜像源在新版本刚发布时同步不完整。请先卸载再从官方源重装:dsh plugin --profile ${config.profile} remove ${name} 然后 add ${name} --registry=https://registry.npmjs.org / ${name} arrived without the entry file its package.json points at; the version pin was rolled back so the next boot still works. A registry mirror serving an incomplete tarball for a just-published version is the usual cause. Remove it and reinstall from the official registry — a plain retry reuses the cached bad artifact.`;
|
|
1074
1113
|
const cancelDiff = cancelled ? changedSince(beforeInstalled) : null;
|
|
1075
1114
|
// Build-script blocks hit updates too (#69): a leftover invalid
|
|
1076
1115
|
// allowBuilds entry (pnpm's placeholder bug, #56) or a newly
|
|
@@ -1090,7 +1129,7 @@ export function mountMarketRoutes(host, config, commandRuntime) {
|
|
|
1090
1129
|
activation,
|
|
1091
1130
|
ignoredBuilds,
|
|
1092
1131
|
staleReason: staleReason ?? undefined,
|
|
1093
|
-
error: staleError ?? undefined,
|
|
1132
|
+
error: brokenEntryError ?? staleError ?? undefined,
|
|
1094
1133
|
exitCode: result.exitCode,
|
|
1095
1134
|
timedOut: result.timedOut,
|
|
1096
1135
|
stdout: result.stdout,
|
package/lib/types/profile.d.ts
CHANGED
|
@@ -82,6 +82,25 @@ export declare function bundlePatchEntryIds(dir: string): string[];
|
|
|
82
82
|
* the DeepSeek model with it.
|
|
83
83
|
*/
|
|
84
84
|
export declare function bundlePatchInsertedIds(dir: string): string[];
|
|
85
|
+
/**
|
|
86
|
+
* `name:` and `id:` rows of the package's declared bundle patch. Line-wise
|
|
87
|
+
* on purpose: the strict hot-mount parser rejects config/expression rows,
|
|
88
|
+
* but for "what does this bundle bring in" any row counts. `insertedIds` is
|
|
89
|
+
* the subset nested under an `insert:` key (#147).
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* Rows of one patch file. Exported because a package may ship its patch at
|
|
93
|
+
* the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
|
|
94
|
+
* patch layer has to read that one by the same rules — a second hand-rolled
|
|
95
|
+
* scan drifted from this one and re-introduced #147 on that path (it closed
|
|
96
|
+
* the insert block only on `id:` lines, so `- disable:` followed by nested
|
|
97
|
+
* ids claimed the neighbour's rows).
|
|
98
|
+
*/
|
|
99
|
+
export declare function parsePatchRows(text: string): {
|
|
100
|
+
names: string[];
|
|
101
|
+
ids: string[];
|
|
102
|
+
insertedIds: string[];
|
|
103
|
+
};
|
|
85
104
|
/** The profile manifest's `dsh.profile.bundles` — what the CLI reconciled. */
|
|
86
105
|
export declare function readProfileBundles(profileDirectory: string): string[];
|
|
87
106
|
/**
|
package/lib/verify.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { readFileSync } from 'node:fs';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { listHotMounts, parseSimplePatch } from './hot.js';
|
|
29
|
-
import { hasDshManifest, hasLoadableEntry, profileDir } from './profile.js';
|
|
29
|
+
import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir } from './profile.js';
|
|
30
30
|
/** The profile manifest's `dsh.profile.bundles` — what the CLI reconciled. */
|
|
31
31
|
function readBundles(profile, explicitDir) {
|
|
32
32
|
try {
|
|
@@ -58,6 +58,28 @@ function liveIncludes(live, packageName) {
|
|
|
58
58
|
return true;
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* True when a loader entry this package's OWN bundle patch inserts is up.
|
|
63
|
+
*
|
|
64
|
+
* A carrier bundle (#103) ships no plugin of its own: its patch mounts
|
|
65
|
+
* ANOTHER package with configuration, so the live entry carries that other
|
|
66
|
+
* package's name and `liveIncludes` can never match. The entry ID is the
|
|
67
|
+
* part that belongs to this package — its patch created it — which is why
|
|
68
|
+
* matching on it is both sufficient and precise: a neighbour that happens
|
|
69
|
+
* to mount the same package does so under a different id.
|
|
70
|
+
*
|
|
71
|
+
* Without this the market kept telling users to restart for a plugin that
|
|
72
|
+
* had been running since the restart (#156).
|
|
73
|
+
*/
|
|
74
|
+
function carriedRowLive(live, profileDirectory, packageName) {
|
|
75
|
+
try {
|
|
76
|
+
return bundlePatchInsertedIds(join(profileDirectory, 'node_modules', packageName))
|
|
77
|
+
.some(id => live.has(`#${id}`));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
61
83
|
function readPkgDsh(profile, name, explicitDir) {
|
|
62
84
|
try {
|
|
63
85
|
const manifest = JSON.parse(readFileSync(join(profileDir(profile, explicitDir), 'node_modules', name, 'package.json'), 'utf8'));
|
|
@@ -106,7 +128,7 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
|
|
|
106
128
|
// loaded by name from a bundle patch — @deepseek-ai/dsh-tools is loaded by
|
|
107
129
|
// the official dsh-base patch and has no `dsh` field at all — so this check
|
|
108
130
|
// has to come before any manifest-based verdict.
|
|
109
|
-
const loaderLive = liveIncludes(live, name);
|
|
131
|
+
const loaderLive = liveIncludes(live, name) || carriedRowLive(live, activeProfileDir, name);
|
|
110
132
|
if (!hasDshManifest(dir)) {
|
|
111
133
|
if (loaderLive) {
|
|
112
134
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.11.
|
|
4
|
+
"version": "1.11.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"plugin-manager"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
|
|
25
25
|
"build": "tsc -p tsconfig.json && npm run build:client",
|
|
26
26
|
"build:client": "tsdown && node scripts/normalize-client-banner.mjs",
|
|
27
27
|
"check": "npm run typecheck && npm run build && node scripts/restart-smoke.mjs",
|
package/src/hot.ts
CHANGED
|
@@ -110,7 +110,13 @@ function readPkgDsh(profileDir: string, packageName: string): { client?: unknown
|
|
|
110
110
|
export function parseSimplePatch(patchText: string): HotRow[] | null {
|
|
111
111
|
const rows: HotRow[] = []
|
|
112
112
|
let pending: string | null = null
|
|
113
|
-
|
|
113
|
+
// Split on CRLF too. `#.*$` cannot strip a comment that ends in \r —
|
|
114
|
+
// JS treats \r as a line terminator, so `.` will not cross it and `$`
|
|
115
|
+
// only anchors at the very end — leaving the comment text in place,
|
|
116
|
+
// matching none of the row shapes, and failing the whole patch. Any
|
|
117
|
+
// plugin whose patch was authored on Windows then reads as
|
|
118
|
+
// "contains config/expression rows" and can never hot-mount.
|
|
119
|
+
for (const raw of patchText.split(/\r?\n/)) {
|
|
114
120
|
const line = raw.replace(/#.*$/, '').trimEnd()
|
|
115
121
|
if (line.trim() === '') continue
|
|
116
122
|
if (/^-\s+insert:\s*$/.test(line)) continue
|
|
@@ -428,7 +434,7 @@ export function readUserPatchControls(profileDir: string): { ids: Set<string>; n
|
|
|
428
434
|
const names = new Set<string>()
|
|
429
435
|
try {
|
|
430
436
|
const text = readFileSync(join(profileDir, 'cordis.patch.yml'), 'utf8')
|
|
431
|
-
for (const line of text.split(
|
|
437
|
+
for (const line of text.split(/\r?\n/)) {
|
|
432
438
|
const id = /^\s*-?\s*id:\s*['"]?([A-Za-z0-9._/@-]+)/.exec(line)
|
|
433
439
|
if (id !== null) ids.add(id[1])
|
|
434
440
|
const name = /^\s*name:\s*['"]?([^'"\s]+)/.exec(line)
|
package/src/patch.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { join } from 'node:path'
|
|
|
30
30
|
import { fileURLToPath } from 'node:url'
|
|
31
31
|
import { logEvent } from './log.ts'
|
|
32
32
|
import { parsePatchFile } from './check.ts'
|
|
33
|
-
import { bundlePatchInsertedIds } from './profile.ts'
|
|
33
|
+
import { bundlePatchInsertedIds, parsePatchRows } from './profile.ts'
|
|
34
34
|
|
|
35
35
|
/** The slice of the loader tree this module needs. */
|
|
36
36
|
export interface PatchHost {
|
|
@@ -205,21 +205,13 @@ export function rowIdsForPackage(host: PatchHost, profileDirectory: string, pack
|
|
|
205
205
|
} catch { /* package not installed — loader side may still know it */ }
|
|
206
206
|
// The conventional location too: a package may ship cordis.patch.yml at
|
|
207
207
|
// its root without declaring dsh.bundle.patch (the loader probes it too).
|
|
208
|
-
// Same
|
|
208
|
+
// Same parser, deliberately: this used to be a second hand-rolled scan
|
|
209
|
+
// that closed the insert block only on `id:` lines, so `- disable:` with
|
|
210
|
+
// nested ids under it claimed the neighbour's rows — #147 all over again
|
|
211
|
+
// on the path #147 did not touch.
|
|
209
212
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const line = raw.replace(/#.*$/u, '')
|
|
213
|
-
if (line.trim() === '') continue
|
|
214
|
-
const indent = line.length - line.trimStart().length
|
|
215
|
-
if (/^\s*-?\s*insert:\s*$/u.test(line)) {
|
|
216
|
-
insertIndent = indent
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
219
|
-
const id = /^\s*-?\s*id:\s*['"]?([A-Za-z0-9_.-]+)/u.exec(line)
|
|
220
|
-
if (id === null) continue
|
|
221
|
-
if (insertIndent !== null && indent > insertIndent) ids.add(id[1])
|
|
222
|
-
else insertIndent = null
|
|
213
|
+
for (const id of parsePatchRows(readFileSync(join(packageDir, 'cordis.patch.yml'), 'utf8')).insertedIds) {
|
|
214
|
+
ids.add(id)
|
|
223
215
|
}
|
|
224
216
|
} catch { /* no conventional patch — nothing more to add */ }
|
|
225
217
|
const prefix = includePrefix(host)
|
package/src/profile.ts
CHANGED
|
@@ -211,27 +211,30 @@ export function bundlePatchInsertedIds(dir: string): string[] {
|
|
|
211
211
|
* but for "what does this bundle bring in" any row counts. `insertedIds` is
|
|
212
212
|
* the subset nested under an `insert:` key (#147).
|
|
213
213
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
} catch {
|
|
224
|
-
return { names: [], ids: [], insertedIds: [] }
|
|
225
|
-
}
|
|
214
|
+
/**
|
|
215
|
+
* Rows of one patch file. Exported because a package may ship its patch at
|
|
216
|
+
* the conventional path INSTEAD of declaring `dsh.bundle.patch`, and the
|
|
217
|
+
* patch layer has to read that one by the same rules — a second hand-rolled
|
|
218
|
+
* scan drifted from this one and re-introduced #147 on that path (it closed
|
|
219
|
+
* the insert block only on `id:` lines, so `- disable:` followed by nested
|
|
220
|
+
* ids claimed the neighbour's rows).
|
|
221
|
+
*/
|
|
222
|
+
export function parsePatchRows(text: string): { names: string[]; ids: string[]; insertedIds: string[] } {
|
|
226
223
|
const names: string[] = []
|
|
227
224
|
const ids: string[] = []
|
|
228
225
|
const insertedIds: string[] = []
|
|
229
|
-
|
|
226
|
+
{
|
|
230
227
|
// `insert:` opens a nested list; every id below it, at deeper
|
|
231
228
|
// indentation, is a row this package brings in. A row at or above the
|
|
232
229
|
// `insert:` indentation closes the block — those target OTHER plugins.
|
|
233
230
|
let insertIndent: number | null = null
|
|
234
|
-
for
|
|
231
|
+
// CRLF too, for consistency with the hot-mount parser, which a
|
|
232
|
+
// Windows-authored patch genuinely broke. Here it changes no outcome
|
|
233
|
+
// today — every row pattern below is `^`-anchored and a comment line
|
|
234
|
+
// always starts with `#`, so an unstripped comment matches nothing —
|
|
235
|
+
// and it is deliberately NOT covered by a spec, because a test that
|
|
236
|
+
// passes with or without the change tests nothing.
|
|
237
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
235
238
|
const line = raw.replace(/#.*$/, '')
|
|
236
239
|
if (line.trim() === '') continue
|
|
237
240
|
const indent = line.length - line.trimStart().length
|
|
@@ -256,10 +259,25 @@ function readBundlePatchRows(dir: string): { names: string[]; ids: string[]; ins
|
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
|
-
}
|
|
262
|
+
}
|
|
260
263
|
return { names, ids, insertedIds }
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
/** Rows of the patch a package DECLARES through `dsh.bundle.patch`. */
|
|
267
|
+
function readBundlePatchRows(dir: string): { names: string[]; ids: string[]; insertedIds: string[] } {
|
|
268
|
+
const empty = { names: [], ids: [], insertedIds: [] }
|
|
269
|
+
try {
|
|
270
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as {
|
|
271
|
+
dsh?: { bundle?: { patch?: unknown } }
|
|
272
|
+
}
|
|
273
|
+
const declared = manifest.dsh?.bundle?.patch
|
|
274
|
+
if (typeof declared !== 'string' || declared === '') return empty
|
|
275
|
+
return parsePatchRows(readFileSync(join(dir, declared), 'utf8'))
|
|
276
|
+
} catch {
|
|
277
|
+
return empty
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
/** The profile manifest's `dsh.profile.bundles` — what the CLI reconciled. */
|
|
264
282
|
export function readProfileBundles(profileDirectory: string): string[] {
|
|
265
283
|
try {
|
|
@@ -396,7 +414,7 @@ export function setAllowBuilds(profile: string, packages: string[], explicitDir?
|
|
|
396
414
|
const map: Record<string, string> = {}
|
|
397
415
|
const blockMatch = blockRe.exec(yaml)
|
|
398
416
|
if (blockMatch !== null) {
|
|
399
|
-
for (const line of blockMatch[1].split(
|
|
417
|
+
for (const line of blockMatch[1].split(/\r?\n/)) {
|
|
400
418
|
// The key itself may contain colons: git-hosted deps are only matched
|
|
401
419
|
// by a `name@git+https://…` key (#68). The anchored boolean tail makes
|
|
402
420
|
// the split land on the LAST colon, never inside a `://` — and doubles
|
package/src/routes.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
BOOT_ID, cancelActive, probePnpm, progress, provisionPnpm, runDshPlugin,
|
|
24
24
|
type PluginCommandRuntime,
|
|
25
25
|
} from './dsh-cli.ts'
|
|
26
|
-
import { INBOX_BUNDLES, profileDir, readInstalled, readInstalledManifest, readInstalledVersion, readLockCommits, readManifestDeps, readProfileBundles, restoreManifestDeps, setAllowBuilds } from './profile.ts'
|
|
26
|
+
import { hasLoadableEntry, INBOX_BUNDLES, profileDir, readInstalled, readInstalledManifest, readInstalledVersion, readLockCommits, readManifestDeps, readProfileBundles, restoreManifestDeps, setAllowBuilds } from './profile.ts'
|
|
27
27
|
import { analyzeProfile } from './check.ts'
|
|
28
28
|
import { applyBundleOrder, mergeOrder, readBundleRules, readBundleStack, validateOrder } from './order.ts'
|
|
29
29
|
import { trialValidate } from './trial.ts'
|
|
@@ -281,7 +281,21 @@ export function mountMarketRoutes(
|
|
|
281
281
|
function liveNames(): Set<string> {
|
|
282
282
|
const live = new Set(listHotMounts())
|
|
283
283
|
for (const entry of host.loader.entries()) {
|
|
284
|
-
if (entry.fiber
|
|
284
|
+
if (entry.fiber === undefined) continue
|
|
285
|
+
if (entry.options.name !== undefined) live.add(entry.options.name)
|
|
286
|
+
// Entry IDS too, under a `#` prefix that cannot collide with a package
|
|
287
|
+
// name. A CARRIER bundle's row names the package it mounts, not
|
|
288
|
+
// itself (#156: @tt-a1i/archify-dsh inserts an entry named
|
|
289
|
+
// @deepseek-ai/dsh-skill-filesystem), so its own name never appears
|
|
290
|
+
// here — but the id it created does, and that id is unique to its
|
|
291
|
+
// patch. Verification needs both, and putting them in one set means
|
|
292
|
+
// no call site can pass the names and forget the ids.
|
|
293
|
+
if (entry.options.id !== undefined && entry.options.id !== '') {
|
|
294
|
+
live.add(`#${entry.options.id}`)
|
|
295
|
+
// Loader ids may carry an include prefix (`include:archify-…`).
|
|
296
|
+
const bare = entry.options.id.split(':').pop()
|
|
297
|
+
if (bare !== undefined && bare !== entry.options.id) live.add(`#${bare}`)
|
|
298
|
+
}
|
|
285
299
|
}
|
|
286
300
|
return live
|
|
287
301
|
}
|
|
@@ -1089,6 +1103,25 @@ export function mountMarketRoutes(
|
|
|
1089
1103
|
})
|
|
1090
1104
|
if (stale) ok = false
|
|
1091
1105
|
}
|
|
1106
|
+
// The new build has to be loadable (#159). pnpm exits 0 for any
|
|
1107
|
+
// tarball it can extract, and the version really did change, so
|
|
1108
|
+
// nothing above notices a package that arrived without its entry
|
|
1109
|
+
// artifact — a registry mirror serving a source-only tarball for
|
|
1110
|
+
// a just-published version is the reported case, a plugin author
|
|
1111
|
+
// shipping a broken `files` list is the other one.
|
|
1112
|
+
//
|
|
1113
|
+
// Activation cannot stand in for this check: a package updating
|
|
1114
|
+
// ITSELF still reports live, because the running fiber belongs to
|
|
1115
|
+
// the OLD code that is already in memory. The failure only
|
|
1116
|
+
// surfaces on the next boot, as a profile that will not start.
|
|
1117
|
+
let brokenEntry = false
|
|
1118
|
+
if (ok && !hasLoadableEntry(activeProfileDir, name)) {
|
|
1119
|
+
brokenEntry = true
|
|
1120
|
+
ok = false
|
|
1121
|
+
const rolledBack = restoreManifestDeps(config.profile, manifestBefore, activeProfileDir)
|
|
1122
|
+
logEvent('error', 'update',
|
|
1123
|
+
`${name}: updated build has no loadable entry — rolled back the pin${rolledBack.length > 0 ? ` (${rolledBack.join(', ')})` : ''}`)
|
|
1124
|
+
}
|
|
1092
1125
|
if (ok) {
|
|
1093
1126
|
invalidateUpdates()
|
|
1094
1127
|
activation = { [name]: verifyActivation(config.profile, name, liveNames(), activeProfileDir, disabled.has(name)) }
|
|
@@ -1104,6 +1137,12 @@ export function mountMarketRoutes(
|
|
|
1104
1137
|
: staleReason === 'release-age'
|
|
1105
1138
|
? '这个新版本刚发布不久。为了安全,系统默认会等它发布满一天后再安装——刚发布的版本偶尔会被发现问题然后撤回。可以明天再试,或点「立即更新」不再等待。 / This version was just released; for safety, installs normally wait about a day after a release. Try again tomorrow, or click "Update now" to install it right away.'
|
|
1106
1139
|
: '更新命令执行完成,但版本没有变化,原因未能确认。点「立即更新」重试通常能解决;若仍不行,请导出日志反馈。 / The update command completed but the version did not change; the cause could not be confirmed. Clicking "Update now" to retry usually resolves it — if not, export the log and report it.'
|
|
1140
|
+
// Actionable, because the user's own recovery is the right one:
|
|
1141
|
+
// the bad artifact is cached under its integrity hash, so a plain
|
|
1142
|
+
// re-add reuses it — the package has to be removed first.
|
|
1143
|
+
const brokenEntryError = !brokenEntry ? null
|
|
1144
|
+
: `${name} 更新后缺少入口文件(package.json 的 main/exports 指向的文件不存在),已回滚版本号以免下次启动失败。这通常是镜像源在新版本刚发布时同步不完整。请先卸载再从官方源重装:dsh plugin --profile ${config.profile} remove ${name} 然后 add ${name} --registry=https://registry.npmjs.org / ${name} arrived without the entry file its package.json points at; the version pin was rolled back so the next boot still works. A registry mirror serving an incomplete tarball for a just-published version is the usual cause. Remove it and reinstall from the official registry — a plain retry reuses the cached bad artifact.`
|
|
1145
|
+
|
|
1107
1146
|
const cancelDiff = cancelled ? changedSince(beforeInstalled) : null
|
|
1108
1147
|
// Build-script blocks hit updates too (#69): a leftover invalid
|
|
1109
1148
|
// allowBuilds entry (pnpm's placeholder bug, #56) or a newly
|
|
@@ -1124,7 +1163,7 @@ export function mountMarketRoutes(
|
|
|
1124
1163
|
activation,
|
|
1125
1164
|
ignoredBuilds,
|
|
1126
1165
|
staleReason: staleReason ?? undefined,
|
|
1127
|
-
error: staleError ?? undefined,
|
|
1166
|
+
error: brokenEntryError ?? staleError ?? undefined,
|
|
1128
1167
|
exitCode: result.exitCode,
|
|
1129
1168
|
timedOut: result.timedOut,
|
|
1130
1169
|
stdout: result.stdout,
|
package/src/verify.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { readFileSync } from 'node:fs'
|
|
28
28
|
import { join } from 'node:path'
|
|
29
29
|
import { listHotMounts, parseSimplePatch } from './hot.ts'
|
|
30
|
-
import { hasDshManifest, hasLoadableEntry, profileDir } from './profile.ts'
|
|
30
|
+
import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir } from './profile.ts'
|
|
31
31
|
|
|
32
32
|
export type ActivationState = 'live' | 'restart' | 'inert' | 'broken' | 'missing' | 'disabled'
|
|
33
33
|
|
|
@@ -77,6 +77,28 @@ function liveIncludes(live: ReadonlySet<string>, packageName: string): boolean {
|
|
|
77
77
|
return false
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* True when a loader entry this package's OWN bundle patch inserts is up.
|
|
82
|
+
*
|
|
83
|
+
* A carrier bundle (#103) ships no plugin of its own: its patch mounts
|
|
84
|
+
* ANOTHER package with configuration, so the live entry carries that other
|
|
85
|
+
* package's name and `liveIncludes` can never match. The entry ID is the
|
|
86
|
+
* part that belongs to this package — its patch created it — which is why
|
|
87
|
+
* matching on it is both sufficient and precise: a neighbour that happens
|
|
88
|
+
* to mount the same package does so under a different id.
|
|
89
|
+
*
|
|
90
|
+
* Without this the market kept telling users to restart for a plugin that
|
|
91
|
+
* had been running since the restart (#156).
|
|
92
|
+
*/
|
|
93
|
+
function carriedRowLive(live: ReadonlySet<string>, profileDirectory: string, packageName: string): boolean {
|
|
94
|
+
try {
|
|
95
|
+
return bundlePatchInsertedIds(join(profileDirectory, 'node_modules', packageName))
|
|
96
|
+
.some(id => live.has(`#${id}`))
|
|
97
|
+
} catch {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
function readPkgDsh(profile: string, name: string, explicitDir?: string): PkgDsh | null {
|
|
81
103
|
try {
|
|
82
104
|
const manifest = JSON.parse(
|
|
@@ -136,7 +158,7 @@ export function verifyActivation(
|
|
|
136
158
|
// loaded by name from a bundle patch — @deepseek-ai/dsh-tools is loaded by
|
|
137
159
|
// the official dsh-base patch and has no `dsh` field at all — so this check
|
|
138
160
|
// has to come before any manifest-based verdict.
|
|
139
|
-
const loaderLive = liveIncludes(live, name)
|
|
161
|
+
const loaderLive = liveIncludes(live, name) || carriedRowLive(live, activeProfileDir, name)
|
|
140
162
|
if (!hasDshManifest(dir)) {
|
|
141
163
|
if (loaderLive) {
|
|
142
164
|
return {
|