dsh-ssh-tunnel 1.0.0

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.
@@ -0,0 +1,226 @@
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
@@ -0,0 +1,106 @@
1
+ // Dev probe: load lib/client.js under a mock __ModuleLoader__, apply() it against
2
+ // a mock cordis ctx, and renderToString the registered tab component tree.
3
+ //
4
+ // The probe needs a real react/react-dom. Not every DSH install ships them as
5
+ // resolvable packages (the web frontend bundles its own copy), so it tries a list
6
+ // of candidate roots and skips cleanly — the same graceful-skip convention the
7
+ // smoke tests use for unavailable symlinks — instead of failing on a hard-coded
8
+ // absolute path.
9
+ //
10
+ // Candidates: $DSH_RUNTIME_ROOT, the profile dir ($DSH_PROFILE_DIR or
11
+ // $DSH_HOME/profiles/${DSH_PROFILE:-web}), then the usual global install roots.
12
+ import vm from "node:vm";
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { createRequire } from "node:module";
17
+
18
+ function reactFrom(root) {
19
+ if (!root) return null;
20
+ try {
21
+ const req = createRequire(path.join(path.resolve(root), "package.json"));
22
+ return {
23
+ React: req("react"),
24
+ ReactDOM: req("react-dom"),
25
+ renderToString: req("react-dom/server").renderToString,
26
+ };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ const profileDir =
33
+ process.env.DSH_PROFILE_DIR ||
34
+ path.join(
35
+ process.env.DSH_HOME || path.join(os.homedir(), ".dsh"),
36
+ "profiles",
37
+ process.env.DSH_PROFILE || "web",
38
+ );
39
+
40
+ const candidates = [
41
+ process.env.DSH_RUNTIME_ROOT,
42
+ profileDir,
43
+ "/usr/local/lib/node_modules/@deepseek-ai/dsh",
44
+ "/usr/lib/node_modules/@deepseek-ai/dsh",
45
+ process.env.APPDATA
46
+ ? path.join(process.env.APPDATA, "npm", "node_modules", "@deepseek-ai", "dsh")
47
+ : "",
48
+ ];
49
+
50
+ let real = null;
51
+ for (const root of candidates) {
52
+ real = reactFrom(root);
53
+ if (real) break;
54
+ }
55
+ if (!real) {
56
+ console.log("portal probe skipped: no resolvable react/react-dom package.");
57
+ console.log(" tried:", candidates.filter(Boolean).join(", "));
58
+ console.log(" set DSH_RUNTIME_ROOT to a root whose node_modules holds react to run it.");
59
+ process.exit(0);
60
+ }
61
+ const { React, ReactDOM, renderToString } = real;
62
+
63
+ let loaded = null;
64
+ const sandbox = {
65
+ window: { __ModuleLoader__: { load(def) { loaded = def; } } },
66
+ console,
67
+ setTimeout,
68
+ clearTimeout,
69
+ setInterval,
70
+ clearInterval,
71
+ };
72
+ sandbox.require = (name) => {
73
+ if (name === "react") return React;
74
+ if (name === "react-dom") return ReactDOM;
75
+ throw new Error("unexpected require in probe: " + name);
76
+ };
77
+ vm.createContext(sandbox);
78
+ const src = fs.readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
79
+ vm.runInContext(src, sandbox, { filename: "client.js" });
80
+
81
+ if (!loaded || loaded.id !== "dsh-ssh-tunnel") throw new Error("module did not register");
82
+ const mod = loaded.factory(sandbox.require);
83
+ if (typeof mod.apply !== "function" || !Array.isArray(mod.inject)) throw new Error("bad exports");
84
+
85
+ const captured = {};
86
+ const ctx = {
87
+ locale: null,
88
+ betterSidebar: { registerTab: (d) => { captured.tab = d; return () => {}; } },
89
+ effect(fn) { return fn(); },
90
+ };
91
+ mod.apply(ctx);
92
+ if (!captured.tab || typeof captured.tab.component !== "function") throw new Error("tab not registered");
93
+
94
+ // zh/en dictionaries register only when ctx.locale exists — the title factory must
95
+ // still return a string through the built-in fallback.
96
+ if (!captured.tab.title || typeof captured.tab.title() !== "string") throw new Error("title fn broken");
97
+
98
+ const el = captured.tab.component({ visible: true, scope: {} });
99
+ const html = renderToString(el);
100
+
101
+ // Markers are the class names the tab tree actually renders (lib/client.js).
102
+ for (const marker of ["ssh-t-root", "ssh-t-head"]) {
103
+ if (!html.includes(marker)) throw new Error("missing sidebar markup: " + marker);
104
+ }
105
+ console.log("probe OK: tab tree renders, length =", html.length);
106
+ console.log("portal available:", typeof ReactDOM.createPortal === "function");