dsh-ssh-tunnel 1.0.0 → 1.0.2

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.
@@ -1,226 +1,278 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # dsh-ssh-tunnel 一键安装(官方 CLI + bundle 自动挂载)
4
- #
5
- # dsh plugin --profile web add <spec>
6
- #
7
- # 包内 dsh.bundle.patch(cordis.patch.yml)会由 CLI 写入
8
- # dsh.profile.bundles,无需手写 profile cordis.patch.yml。
9
- #
10
- # 用法:
11
- # bash scripts/install.sh [版本] [--restart] [--dry-run] [--from github|npm]
12
- #
13
- # 版本 npm/git 版本;github 源可省略(默认 HEAD)或传 branch/tag/commit
14
- # --from github(默认,仓库尚未上 npm 时)| npm
15
- # --restart 尝试 pm2 restart dsh-web
16
- # --dry-run 只打印步骤
17
- #
18
- # 环境:DSH_HOME(默认 ~/.dsh)、REGISTRY、DSH_CMD、GITHUB_REPO
19
- # =============================================================================
20
- set -euo pipefail
21
-
22
- for arg in "$@"; do
23
- if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
24
- cat <<'EOF'
25
- dsh-ssh-tunnel 一键安装
26
-
27
- 用法:bash scripts/install.sh [版本] [--restart] [--dry-run] [--from github|npm]
28
-
29
- 版本 缺省:github 用默认分支;npm 用 latest
30
- --from github(默认)| npm
31
- --restart 装完尝试 pm2 restart dsh-web
32
- --dry-run 只打印操作
33
-
34
- 前置:已安装并运行过 dsh web;建议已装 dsh-better-sidebar(侧栏 Tab 依赖)。
35
- EOF
36
- exit 0
37
- fi
38
- done
39
-
40
- PKG="dsh-ssh-tunnel"
41
- PLUGIN_ID="ssh-tunnel"
42
- GITHUB_REPO="${GITHUB_REPO:-OMSociety/dsh-ssh-tunnel}"
43
- DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
44
- PROFILE_DIR="$DSH_HOME/profiles/web"
45
- WS_YML="$PROFILE_DIR/pnpm-workspace.yaml"
46
- PATCH_YML="$PROFILE_DIR/cordis.patch.yml"
47
- REGISTRY="${REGISTRY:-https://registry.npmjs.org}"
48
- DSH_CMD="${DSH_CMD:-dsh}"
49
-
50
- RESTART=false
51
- DRY_RUN=false
52
- FROM="github"
53
- VERSION_SPEC=""
54
- while [ $# -gt 0 ]; do
55
- case "$1" in
56
- --restart) RESTART=true; shift ;;
57
- --dry-run) DRY_RUN=true; shift ;;
58
- --from)
59
- [ $# -ge 2 ] || { echo "--from 需要 github|npm" >&2; exit 2; }
60
- FROM="$2"; shift 2
61
- ;;
62
- --from=github) FROM="github"; shift ;;
63
- --from=npm) FROM="npm"; shift ;;
64
- -h|--help) shift ;;
65
- -*)
66
- echo "未知参数: ${1}(用 -h 查看用法)" >&2
67
- exit 2
68
- ;;
69
- *) VERSION_SPEC="$1"; shift ;;
70
- esac
71
- done
72
- case "$FROM" in github|npm) ;; *) echo "--from 只能是 github 或 npm" >&2; exit 2 ;; esac
73
-
74
- say() { printf '\033[32m[install]\033[0m %s\n' "$*"; }
75
- warn() { printf '\033[33m[warn]\033[0m %s\n' "$*" >&2; }
76
- die() { printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
77
-
78
- dsh_cli() {
79
- if command -v "$DSH_CMD" >/dev/null 2>&1; then
80
- printf '%s' "$DSH_CMD"
81
- elif command -v npx >/dev/null 2>&1; then
82
- printf 'npx -y --package @deepseek-ai/dsh dsh'
83
- else
84
- die "未找到 dsh 或 npx。请先安装 DSH,或用 DSH_CMD 指定。"
85
- fi
86
- }
87
-
88
- resolve_add_spec() {
89
- if [ "$FROM" = "npm" ]; then
90
- local given="${VERSION_SPEC:-latest}"
91
- if [ "$given" = "latest" ]; then
92
- local v=""
93
- if command -v npm >/dev/null 2>&1; then
94
- v="$(npm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
95
- fi
96
- if [ -z "$v" ] && command -v pnpm >/dev/null 2>&1; then
97
- v="$(pnpm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
98
- fi
99
- if [ -n "$v" ]; then printf '%s@%s' "$PKG" "$v"
100
- else printf '%s@latest' "$PKG"
101
- fi
102
- else
103
- printf '%s@%s' "$PKG" "$given"
104
- fi
105
- else
106
- local ref="${VERSION_SPEC:-}"
107
- if [ -n "$ref" ]; then
108
- printf '%s@github:%s#%s' "$PKG" "$GITHUB_REPO" "$ref"
109
- else
110
- printf '%s@github:%s' "$PKG" "$GITHUB_REPO"
111
- fi
112
- fi
113
- }
114
-
115
- command -v node >/dev/null 2>&1 || die "未找到 node(需要 Node.js ≥ 20)。"
116
- NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || NODE_MAJOR=0
117
- [ "${NODE_MAJOR:-0}" -ge 20 ] || die "Node.js $(node -v) 过旧(需要 ≥ 20)。"
118
- [ -d "$PROFILE_DIR" ] || die "找不到 profile 目录:${PROFILE_DIR}(请先运行过 dsh web)"
119
- [ -f "$WS_YML" ] || die "找不到 ${WS_YML}(请先初始化 web profile)"
120
-
121
- ADD_SPEC="$(resolve_add_spec)"
122
- CLI="$(dsh_cli)"
123
- say "目标:$CLI plugin --profile web add ${ADD_SPEC}(profile: ${PROFILE_DIR})"
124
- say "来源:${FROM}(仓库 ${GITHUB_REPO})"
125
-
126
- if [ "$DRY_RUN" = true ]; then
127
- say "[dry-run] 1) minimumReleaseAgeExclude += ${PKG}"
128
- say "[dry-run] 2) $CLI plugin --profile web add ${ADD_SPEC}"
129
- say "[dry-run] 3) 校验 dsh.profile.bundles 含 ${PKG}"
130
- say "[dry-run] 4) 移除 profile cordis.patch.yml 中 id: ${PLUGIN_ID} 的旧手动挂载"
131
- if [ "$RESTART" = true ]; then say "[dry-run] 5) pm2 restart dsh-web"; else say "[dry-run] 5) 提示手动重启"; fi
132
- exit 0
133
- fi
134
-
135
- WS_RESULT="$(node -e '
136
- const fs = require("fs");
137
- const p = process.argv[1];
138
- const pkg = process.argv[2];
139
- let t = fs.readFileSync(p, "utf8");
140
- const before = t;
141
- if (!new RegExp("^\\s*-\\s+" + pkg.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&") + "\\s*$", "m").test(t)) {
142
- if (/^\\s*minimumReleaseAgeExclude:\\s*$/m.test(t)) {
143
- t = t.replace(/^(\\s*minimumReleaseAgeExclude:\\s*)$/m, "$1\\n - " + pkg);
144
- } else {
145
- t += "\\nminimumReleaseAgeExclude:\\n - " + pkg + "\\n";
146
- }
147
- }
148
- if (t !== before) fs.writeFileSync(p, t);
149
- console.log(t === before ? "unchanged" : "updated");
150
- ' "$WS_YML" "$PKG")"
151
- [ "$WS_RESULT" = "updated" ] \
152
- && say "已写入 ${WS_YML}:minimumReleaseAgeExclude(${PKG})" \
153
- || say "workspace 设置已就绪,跳过"
154
-
155
- say "执行 $CLI plugin --profile web add $ADD_SPEC ..."
156
- if ! $CLI plugin --profile web add "$ADD_SPEC" 2>&1 | tail -n +1; then
157
- warn "dsh plugin add 失败。可检查网络、registry,或手动:"
158
- warn " cd $PROFILE_DIR && pnpm install"
159
- warn "前置建议:dsh plugin --profile web add dsh-better-sidebar"
160
- exit 1
161
- fi
162
-
163
- if ! node -e '
164
- const fs = require("fs");
165
- const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
166
- const bundles = p.dsh?.profile?.bundles ?? [];
167
- process.exit(bundles.includes(process.argv[2]) ? 0 : 1);
168
- ' "$PROFILE_DIR/package.json" "$PKG"; then
169
- warn "${PKG} 未出现在 dsh.profile.bundles——挂载未注册。"
170
- exit 1
171
- fi
172
- say "bundle 已注册:dsh.profile.bundles 包含 ${PKG}"
173
-
174
- if [ -f "$PATCH_YML" ]; then
175
- MOUNT_RESULT="$(node -e '
176
- const fs = require("fs");
177
- const p = process.argv[1];
178
- const id = process.argv[2];
179
- const lines = fs.readFileSync(p, "utf8").split("\\n");
180
- const out = [];
181
- let i = 0;
182
- let removed = false;
183
- const idRe = new RegExp("id:\\\\s*" + id.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&") + "\\\\b");
184
- while (i < lines.length) {
185
- const line = lines[i];
186
- if (/^[ \\t]*- insert:\\s*$/.test(line)) {
187
- const block = [line];
188
- let j = i + 1;
189
- while (j < lines.length && lines[j].trim() !== "" && !/^-\\s/.test(lines[j])) {
190
- block.push(lines[j]);
191
- j++;
192
- }
193
- if (block.some((l) => idRe.test(l))) {
194
- while (out.length && /^[ \\t]*#/.test(out[out.length - 1])) out.pop();
195
- i = j;
196
- removed = true;
197
- continue;
198
- }
199
- }
200
- out.push(line);
201
- i++;
202
- }
203
- if (!removed) console.log("none");
204
- else {
205
- fs.writeFileSync(p, out.join("\\n").replace(/\\n{3,}/g, "\\n\\n"));
206
- console.log("removed");
207
- }
208
- ' "$PATCH_YML" "$PLUGIN_ID")"
209
- [ "$MOUNT_RESULT" = "removed" ] \
210
- && say "已从 $PATCH_YML 移除旧的 ${PLUGIN_ID} 手动挂载行" \
211
- || say "无旧手动挂载行,跳过"
212
- fi
213
-
214
- say "安装完成:${ADD_SPEC}"
215
- say "验证:dsh --profile web --dump-config | grep -n '${PLUGIN_ID}\\|${PKG}'"
216
-
217
- if [ "$RESTART" = true ]; then
218
- if command -v pm2 >/dev/null 2>&1; then
219
- say "重启 dsh-web(pm2)..."
220
- pm2 restart dsh-web || warn "pm2 restart 失败,请手动重启 DSH"
221
- else
222
- warn "未找到 pm2,请手动重启 DSH(docker compose restart / pm2 / 进程管理器)"
223
- fi
224
- else
225
- say "下一步:重启 DSH web,并硬刷新浏览器(Cmd/Ctrl+Shift+R)。"
226
- fi
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # dsh-ssh-tunnel 一键安装(官方 CLI + bundle 自动挂载)
4
+ #
5
+ # dsh plugin --profile <名称> add <spec>
6
+ #
7
+ # 包内 dsh.bundle.patch(cordis.patch.yml)会由 CLI 写入
8
+ # dsh.profile.bundles,无需手写 profile cordis.patch.yml。
9
+ #
10
+ # 用法:
11
+ # bash scripts/install.sh [版本] [--profile <名称>] [--fix-profile] [--from github|npm] [--restart] [--dry-run] [-h|--help]
12
+ #
13
+ # 版本 npm 版本或 github 的 branch/tag/commit;github 源可省略(默认 HEAD)
14
+ # --profile 目标 profile 名称(必填,无默认值;缺失或不存在时列出实存 profile 并退出 2)
15
+ # --fix-profile 可选维护动作:向 pnpm-workspace.yaml 的 minimumReleaseAgeExclude
16
+ # 追加本包(幂等),并清理 cordis.patch.yml 里 id: ssh-tunnel 的
17
+ # 旧手动挂载块(幂等)。两者均写后回读断言,失败回滚并以非零码退出。
18
+ # 不带本参数时绝不改写这两个文件,只在缺条目时打 warn 提示。
19
+ # --from github(默认,仓库尚未上 npm 时)| npm
20
+ # --restart 尝试 pm2 restart dsh-web
21
+ # --dry-run 只打印步骤,不写任何文件
22
+ #
23
+ # 环境:DSH_HOME(默认 ~/.dsh)、DSH_CMD(默认 dsh)、REGISTRY、GITHUB_REPO、
24
+ # DSH_INSTALL_YES=1(跳过 npx 兜底的交互确认)
25
+ # =============================================================================
26
+ set -euo pipefail
27
+
28
+ PKG="dsh-ssh-tunnel"
29
+ PLUGIN_ID="ssh-tunnel"
30
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
31
+ LIB_DIR="$SCRIPT_DIR/lib"
32
+ GITHUB_REPO="${GITHUB_REPO:-OMSociety/dsh-ssh-tunnel}"
33
+ REGISTRY="${REGISTRY:-https://registry.npmjs.org}"
34
+ DSH_CMD="${DSH_CMD:-dsh}"
35
+ DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
36
+
37
+ usage() {
38
+ cat <<'EOF'
39
+ dsh-ssh-tunnel 一键安装
40
+
41
+ 用法:bash scripts/install.sh [版本] [--profile <名称>] [--fix-profile] [--from github|npm] [--restart] [--dry-run] [-h|--help]
42
+
43
+ 版本 npm 版本或 github 的 branch/tag/commit;github 源可省略(默认 HEAD)
44
+ --profile <名> 目标 profile 名称(必填,无默认值)
45
+ --fix-profile 可选:补写 minimumReleaseAgeExclude 并清理旧手动挂载(幂等、带回读断言)
46
+ --from github(默认)| npm
47
+ --restart 装完尝试 pm2 restart dsh-web
48
+ --dry-run 只打印操作,不写任何文件
49
+ -h|--help 显示本帮助
50
+
51
+ 环境:DSH_HOME(默认 ~/.dsh)、DSH_CMD(默认 dsh)、REGISTRY、GITHUB_REPO、
52
+ DSH_INSTALL_YES=1(跳过 npx 兜底确认)
53
+
54
+ 前置:目标 profile 已初始化(含 package.json 与 pnpm-workspace.yaml);
55
+ 建议已装 dsh-better-sidebar(侧栏 Tab 依赖)。
56
+ EOF
57
+ }
58
+
59
+ say() { printf '\033[32m[install]\033[0m %s\n' "$*"; }
60
+ warn() { printf '\033[33m[warn]\033[0m %s\n' "$*" >&2; }
61
+ die() { printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
62
+
63
+ list_profiles() {
64
+ if [ ! -d "$DSH_HOME/profiles" ]; then
65
+ echo " (未找到 $DSH_HOME/profiles)"
66
+ return 0
67
+ fi
68
+ local any="" d
69
+ for d in "$DSH_HOME"/profiles/*/; do
70
+ [ -d "$d" ] || continue
71
+ any=1
72
+ echo " - $(basename "$d")"
73
+ done
74
+ [ -n "$any" ] || echo " ($DSH_HOME/profiles 下没有 profile)"
75
+ }
76
+
77
+ # ---------- 参数解析 ----------
78
+ PROFILE_NAME=""
79
+ FROM="github"
80
+ VERSION_SPEC=""
81
+ FIX_PROFILE=false
82
+ RESTART=false
83
+ DRY_RUN=false
84
+ SHOW_HELP=false
85
+
86
+ while [ $# -gt 0 ]; do
87
+ case "$1" in
88
+ -h|--help) SHOW_HELP=true; shift ;;
89
+ --profile)
90
+ [ $# -ge 2 ] || { echo "[error] --profile 需要一个名称(用 -h 查看用法)" >&2; exit 2; }
91
+ PROFILE_NAME="$2"; shift 2 ;;
92
+ --profile=*) PROFILE_NAME="${1#*=}"; shift ;;
93
+ --fix-profile) FIX_PROFILE=true; shift ;;
94
+ --from)
95
+ [ $# -ge 2 ] || { echo "[error] --from 需要 github|npm(用 -h 查看用法)" >&2; exit 2; }
96
+ FROM="$2"; shift 2 ;;
97
+ --from=*) FROM="${1#*=}"; shift ;;
98
+ --restart) RESTART=true; shift ;;
99
+ --dry-run) DRY_RUN=true; shift ;;
100
+ -*) echo "[error] 未知参数: $1(用 -h 查看用法)" >&2; exit 2 ;;
101
+ *)
102
+ if [ -n "$VERSION_SPEC" ]; then
103
+ echo "[error] 多余的位置参数: $1(版本只能传一个,用 -h 查看用法)" >&2
104
+ exit 2
105
+ fi
106
+ VERSION_SPEC="$1"; shift ;;
107
+ esac
108
+ done
109
+
110
+ if [ "$SHOW_HELP" = true ]; then usage; exit 0; fi
111
+
112
+ case "$FROM" in
113
+ github|npm) ;;
114
+ *) echo "[error] --from 只能是 github 或 npm(当前: $FROM)" >&2; exit 2 ;;
115
+ esac
116
+
117
+ if [ -z "$PROFILE_NAME" ]; then
118
+ echo "[error] 缺少 --profile 参数。$DSH_HOME/profiles/ 下实存 profile:" >&2
119
+ list_profiles >&2
120
+ exit 2
121
+ fi
122
+
123
+ PROFILE_DIR="$DSH_HOME/profiles/$PROFILE_NAME"
124
+ WS_YML="$PROFILE_DIR/pnpm-workspace.yaml"
125
+ PATCH_YML="$PROFILE_DIR/cordis.patch.yml"
126
+ PKG_JSON="$PROFILE_DIR/package.json"
127
+
128
+ if [ ! -d "$PROFILE_DIR" ]; then
129
+ echo "[error] profile 不存在: $PROFILE_NAME($PROFILE_DIR)。$DSH_HOME/profiles/ 下实存 profile:" >&2
130
+ list_profiles >&2
131
+ exit 2
132
+ fi
133
+
134
+ # ---------- 前置检查 ----------
135
+ command -v node >/dev/null 2>&1 || die "未找到 node(需要 Node.js ≥ 20)。"
136
+ NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || NODE_MAJOR=0
137
+ [ "${NODE_MAJOR:-0}" -ge 20 ] || die "Node.js $(node -v 2>/dev/null) 过旧(需要 ≥ 20)。"
138
+
139
+ resolve_add_spec() {
140
+ if [ "$FROM" = "npm" ]; then
141
+ local given="${VERSION_SPEC:-latest}"
142
+ if [ "$given" = "latest" ]; then
143
+ local v=""
144
+ if command -v npm >/dev/null 2>&1; then
145
+ v="$(npm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
146
+ fi
147
+ if [ -z "$v" ] && command -v pnpm >/dev/null 2>&1; then
148
+ v="$(pnpm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
149
+ fi
150
+ if [ -n "$v" ]; then printf '%s@%s' "$PKG" "$v"
151
+ else printf '%s@latest' "$PKG"
152
+ fi
153
+ else
154
+ printf '%s@%s' "$PKG" "$given"
155
+ fi
156
+ else
157
+ local ref="${VERSION_SPEC:-}"
158
+ if [ -n "$ref" ]; then
159
+ printf '%s@github:%s#%s' "$PKG" "$GITHUB_REPO" "$ref"
160
+ else
161
+ printf '%s@github:%s' "$PKG" "$GITHUB_REPO"
162
+ fi
163
+ fi
164
+ }
165
+ ADD_SPEC="$(resolve_add_spec)"
166
+
167
+ # ---------- dsh CLI 解析(npx 兜底需交互确认) ----------
168
+ DSH_CLI_CMD=""
169
+ NPX_FALLBACK=false
170
+ if command -v "$DSH_CMD" >/dev/null 2>&1; then
171
+ DSH_CLI_CMD="$DSH_CMD"
172
+ elif command -v npx >/dev/null 2>&1; then
173
+ DSH_CLI_CMD="npx -y --package @deepseek-ai/dsh dsh"
174
+ NPX_FALLBACK=true
175
+ else
176
+ die "未找到 dsh 或 npx。请先安装 DSH,或用 DSH_CMD 指定。"
177
+ fi
178
+
179
+ say "目标:$DSH_CLI_CMD plugin --profile $PROFILE_NAME add $ADD_SPEC(profile 目录: $PROFILE_DIR)"
180
+ say "来源:$FROM(仓库 $GITHUB_REPO)"
181
+
182
+ if [ "$DRY_RUN" = true ]; then
183
+ say "[dry-run] 1) minimumReleaseAgeExclude 处理(--fix-profile 时写入,否则仅检查+warn):$WS_YML"
184
+ say "[dry-run] 2) $DSH_CLI_CMD plugin --profile $PROFILE_NAME add $ADD_SPEC"
185
+ say "[dry-run] 3) 校验 dsh.profile.bundles 含 $PKG:$PKG_JSON"
186
+ say "[dry-run] 4) allowBuilds 预检(advisory):$PROFILE_DIR/node_modules/.modules.yaml"
187
+ say "[dry-run] 5) 旧手动挂载清理(仅 --fix-profile):$PATCH_YML"
188
+ if [ "$RESTART" = true ]; then
189
+ say "[dry-run] 6) pm2 restart dsh-web"
190
+ else
191
+ say "[dry-run] 6) 提示重启 DSH web 并硬刷新浏览器"
192
+ fi
193
+ exit 0
194
+ fi
195
+
196
+ # npx 兜底:任何写操作发生前先交互确认
197
+ if [ "$NPX_FALLBACK" = true ]; then
198
+ say "未找到 dsh,将用 npx 兜底执行:$DSH_CLI_CMD plugin --profile $PROFILE_NAME add $ADD_SPEC"
199
+ if [ "${DSH_INSTALL_YES:-0}" != "1" ]; then
200
+ printf '继续执行将联网下载并运行 @deepseek-ai/dsh,确认? [y/N] '
201
+ REPLY=""
202
+ read -r REPLY || { echo; die "读取确认失败,中止。"; }
203
+ case "$REPLY" in
204
+ y|Y|yes|YES|Yes) ;;
205
+ *) die "已中止(未执行任何安装步骤)。" ;;
206
+ esac
207
+ fi
208
+ fi
209
+
210
+ [ -f "$PKG_JSON" ] || die "找不到 $PKG_JSON(profile 未初始化?)"
211
+ if [ "$FIX_PROFILE" = true ] && [ ! -f "$WS_YML" ]; then
212
+ die "找不到 $WS_YML(--fix-profile 需要它来补写 minimumReleaseAgeExclude)"
213
+ fi
214
+
215
+ # ---------- 1) minimumReleaseAgeExclude ----------
216
+ if [ "$FIX_PROFILE" = true ]; then
217
+ WS_RESULT="$(node "$LIB_DIR/ws-exclude.cjs" "$WS_YML" "$PKG")" \
218
+ || die "--fix-profile:补写 minimumReleaseAgeExclude 失败(文件已保持原样)。"
219
+ case "$WS_RESULT" in
220
+ updated) say "已补写 $WS_YML:minimumReleaseAgeExclude += $PKG" ;;
221
+ unchanged) say "minimumReleaseAgeExclude 已含 $PKG,跳过" ;;
222
+ *) say "workspace exclude 状态: $WS_RESULT" ;;
223
+ esac
224
+ else
225
+ if [ -f "$WS_YML" ]; then
226
+ if ! node "$LIB_DIR/ws-exclude.cjs" --check "$WS_YML" "$PKG" >/dev/null 2>&1; then
227
+ warn "minimumReleaseAgeExclude 未包含 ${PKG}——pnpm minimumReleaseAge 可能拒绝安装;可加 --fix-profile 自动补写。"
228
+ fi
229
+ else
230
+ warn "未找到 $WS_YML,跳过 minimumReleaseAgeExclude 检查。"
231
+ fi
232
+ fi
233
+
234
+ # ---------- 2) plugin add ----------
235
+ say "执行 $DSH_CLI_CMD plugin --profile $PROFILE_NAME add $ADD_SPEC ..."
236
+ read -r -a CLI_ARR <<< "$DSH_CLI_CMD"
237
+ if ! "${CLI_ARR[@]}" plugin --profile "$PROFILE_NAME" add "$ADD_SPEC"; then
238
+ warn "dsh plugin add 失败。可检查网络、registry,或手动执行:"
239
+ warn " $DSH_CLI_CMD plugin --profile $PROFILE_NAME add \"$ADD_SPEC\""
240
+ warn "前置建议:dsh plugin --profile $PROFILE_NAME add dsh-better-sidebar"
241
+ exit 1
242
+ fi
243
+
244
+ # ---------- 3) bundle 校验 ----------
245
+ if ! node "$LIB_DIR/bundle-check.cjs" "$PKG_JSON" "$PKG"; then
246
+ warn "$PKG 未出现在 dsh.profile.bundles——挂载未注册。"
247
+ exit 1
248
+ fi
249
+ say "bundle 已注册:dsh.profile.bundles 包含 $PKG"
250
+
251
+ # ---------- 4) allowBuilds 预检(advisory) ----------
252
+ if ! node "$LIB_DIR/bundle-check.cjs" --ignored-builds "$PROFILE_DIR"; then
253
+ warn "allowBuilds 预检异常(不影响安装结果)。"
254
+ fi
255
+
256
+ # ---------- 5) 旧手动挂载清理(仅 --fix-profile) ----------
257
+ if [ "$FIX_PROFILE" = true ] && [ -f "$PATCH_YML" ]; then
258
+ STRIP_RESULT="$(node "$LIB_DIR/strip-mount.cjs" "$PATCH_YML" "$PLUGIN_ID")" \
259
+ || die "清理旧手动挂载失败($PATCH_YML 已回滚为原内容;插件本体安装不受影响)。"
260
+ case "$STRIP_RESULT" in
261
+ removed) say "已从 $PATCH_YML 移除旧的 $PLUGIN_ID 手动挂载块" ;;
262
+ none) say "无旧手动挂载块,跳过" ;;
263
+ esac
264
+ fi
265
+
266
+ say "安装完成:$ADD_SPEC(profile: $PROFILE_NAME)"
267
+ say "验证:dsh --profile $PROFILE_NAME --dump-config | grep -n '${PLUGIN_ID}\\|${PKG}'"
268
+
269
+ if [ "$RESTART" = true ]; then
270
+ if command -v pm2 >/dev/null 2>&1; then
271
+ say "重启 dsh-web(pm2)..."
272
+ pm2 restart dsh-web || warn "pm2 restart 失败,请手动重启 DSH"
273
+ else
274
+ warn "未找到 pm2,请手动重启 DSH(docker compose restart / pm2 / 进程管理器)"
275
+ fi
276
+ else
277
+ say "下一步:重启 DSH web,并硬刷新浏览器(Cmd/Ctrl+Shift+R)。"
278
+ fi
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /*
4
+ * 安装后的两类检查(install.sh / install.ps1 共用)。
5
+ *
6
+ * 1) node bundle-check.cjs <profile/package.json> <pkg>
7
+ * 校验 dsh.profile.bundles 是否包含 pkg:包含退出 0(打印 ok),否则退出 1。
8
+ *
9
+ * 2) node bundle-check.cjs --ignored-builds <profileDir>
10
+ * 读取 <profileDir>/node_modules/.modules.yaml 的 ignoredBuilds 段,
11
+ * 打印 allowBuilds 可复制豁免指引。advisory 性质:恒退出 0;
12
+ * .modules.yaml 不存在时跳过不报错。
13
+ */
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ function err2(msg) {
18
+ process.stderr.write('[bundle-check] ' + msg + '\n');
19
+ }
20
+
21
+ const argv = process.argv.slice(2);
22
+
23
+ if (argv[0] === '--ignored-builds') {
24
+ const profileDir = argv[1];
25
+ if (!profileDir || argv.length !== 2) {
26
+ process.stderr.write('用法: node bundle-check.cjs --ignored-builds <profileDir>\n');
27
+ process.exit(2);
28
+ }
29
+ const modulesYaml = path.join(profileDir, 'node_modules', '.modules.yaml');
30
+ let text;
31
+ try {
32
+ text = fs.readFileSync(modulesYaml, 'utf8');
33
+ } catch (e) {
34
+ console.log('[allowBuilds] 未找到 ' + modulesYaml + ',跳过 allowBuilds 预检。');
35
+ process.exit(0);
36
+ }
37
+ const lines = text.split('\n');
38
+ let start = -1;
39
+ let inlineRest = '';
40
+ for (let i = 0; i < lines.length; i++) {
41
+ const m = lines[i].match(/^ignoredBuilds:(.*)$/);
42
+ if (m) {
43
+ start = i;
44
+ inlineRest = m[1].trim();
45
+ break;
46
+ }
47
+ }
48
+ const entries = [];
49
+ if (start !== -1) {
50
+ if (inlineRest && inlineRest !== '[]') {
51
+ // 内联形式(如 ignoredBuilds: [a, b])
52
+ const inner = inlineRest.replace(/^\[/, '').replace(/\]$/, '');
53
+ for (const part of inner.split(',')) {
54
+ const v = part.trim().replace(/^['"]|['"]$/g, '');
55
+ if (v) entries.push(v);
56
+ }
57
+ } else if (!inlineRest) {
58
+ for (let j = start + 1; j < lines.length; j++) {
59
+ if (lines[j].trim() === '') continue;
60
+ const im = lines[j].match(/^[ \t]+-[ \t]*(.+)$/);
61
+ if (!im) break;
62
+ const v = im[1].trim().replace(/^['"]|['"]$/g, '');
63
+ if (v) entries.push(v);
64
+ }
65
+ }
66
+ }
67
+ if (entries.length === 0) {
68
+ console.log('[allowBuilds] ignoredBuilds 为空或不存在,无需处理。');
69
+ process.exit(0);
70
+ }
71
+ const wsPath = path.join(profileDir, 'pnpm-workspace.yaml');
72
+ const out = [];
73
+ out.push('[allowBuilds] pnpm 跳过了以下依赖的构建脚本(node_modules/.modules.yaml: ignoredBuilds):');
74
+ for (const e of entries) out.push(' - ' + e);
75
+ out.push('这些包的 install 脚本没有执行,相关功能可能不完整。如需豁免,把下面内容并入 ' + wsPath + '(已有 allowBuilds 段则合并键值):');
76
+ out.push('');
77
+ out.push('allowBuilds:');
78
+ for (const e of entries) out.push(' "' + e + '": true');
79
+ out.push('');
80
+ out.push('注意: 键必须是上面 ignoredBuilds 行的原文(与 lockfile 中的 spec 逐字一致);纯包名(如 ssh2)不生效。');
81
+ console.log(out.join('\n'));
82
+ process.exit(0);
83
+ }
84
+
85
+ // 模式 1:bundle 校验
86
+ const pkgJsonPath = argv[0];
87
+ const pkg = argv[1];
88
+ if (!pkgJsonPath || !pkg || argv.length !== 2) {
89
+ process.stderr.write('用法: node bundle-check.cjs <profile/package.json> <pkg>\n');
90
+ process.exit(2);
91
+ }
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
95
+ } catch (e) {
96
+ err2('读取/解析失败: ' + pkgJsonPath + ' (' + e.message + ')');
97
+ process.exit(2);
98
+ }
99
+ const bundles =
100
+ parsed && parsed.dsh && parsed.dsh.profile && Array.isArray(parsed.dsh.profile.bundles)
101
+ ? parsed.dsh.profile.bundles
102
+ : null;
103
+ if (bundles && bundles.indexOf(pkg) !== -1) {
104
+ console.log('ok: dsh.profile.bundles contains ' + pkg);
105
+ process.exit(0);
106
+ }
107
+ err2('dsh.profile.bundles 不含 ' + pkg + '(当前: ' + JSON.stringify(bundles) + ')');
108
+ process.exit(1);