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.
- package/CHANGELOG.md +305 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/README_en.md +171 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +1472 -0
- package/lib/index.js +1611 -0
- package/lib/session.js +525 -0
- package/lib/shared/host-key.js +32 -0
- package/lib/shared/host-summary.js +75 -0
- package/lib/shared/http-trust.js +59 -0
- package/lib/shared/path.js +156 -0
- package/lib/shared/persist.js +41 -0
- package/lib/shared/session-auth.js +39 -0
- package/lib/shared/session-policy.js +125 -0
- package/lib/shared/shell-buffer.js +10 -0
- package/lib/shared/vendor.js +50 -0
- package/package.json +84 -0
- package/scripts/install.ps1 +148 -0
- package/scripts/install.sh +226 -0
- package/scripts/portal-probe.mjs +106 -0
- package/scripts/smoke-test.mjs +511 -0
- package/scripts/sync-to-dsh.sh +32 -0
|
@@ -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");
|