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.
- package/CHANGELOG.md +68 -0
- package/README.md +80 -38
- package/README_en.md +80 -38
- package/cordis.patch.yml +4 -4
- package/icon.svg +26 -0
- package/lib/client.js +1643 -1472
- package/lib/index.js +1739 -1611
- package/lib/session.js +616 -525
- package/lib/shared/args.js +23 -0
- package/lib/shared/host-key.js +32 -32
- package/lib/shared/host-summary.js +75 -75
- package/lib/shared/http-trust.js +59 -59
- package/lib/shared/path.js +21 -22
- package/lib/shared/persist.js +41 -41
- package/lib/shared/session-auth.js +47 -39
- package/lib/shared/session-policy.js +151 -125
- package/lib/shared/shell-buffer.js +10 -10
- package/lib/shared/vendor.js +49 -49
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +91 -84
- package/scripts/install.ps1 +191 -93
- package/scripts/install.sh +278 -226
- package/scripts/lib/bundle-check.cjs +108 -0
- package/scripts/lib/strip-mount.cjs +114 -0
- package/scripts/lib/ws-exclude.cjs +143 -0
- package/scripts/smoke-test.mjs +899 -511
- package/scripts/sync-to-dsh.sh +87 -5
package/scripts/install.ps1
CHANGED
|
@@ -1,50 +1,123 @@
|
|
|
1
1
|
# =============================================================================
|
|
2
|
-
# dsh-ssh-tunnel one-click
|
|
2
|
+
# dsh-ssh-tunnel one-click installer (Windows PowerShell 5.1+ and pwsh 7+)
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install.ps1 `
|
|
6
|
+
# -Profile <name> [-Version <ver>] [-From github|npm] [-FixProfile] [-Restart] [-DryRun]
|
|
7
|
+
#
|
|
8
|
+
# Mirrors scripts/install.sh. All JS helpers live in scripts\lib\*.cjs
|
|
9
|
+
# (no inline JS here; every external call is followed by a $LASTEXITCODE check).
|
|
7
10
|
# =============================================================================
|
|
8
11
|
param(
|
|
9
12
|
[string]$Version = '',
|
|
10
13
|
[ValidateSet('github', 'npm')]
|
|
11
14
|
[string]$From = 'github',
|
|
15
|
+
[string]$Profile = '',
|
|
16
|
+
[switch]$FixProfile,
|
|
12
17
|
[switch]$Restart,
|
|
13
|
-
[switch]$DryRun
|
|
18
|
+
[switch]$DryRun,
|
|
19
|
+
[switch]$H,
|
|
20
|
+
[Parameter(ValueFromRemainingArguments = $true)]
|
|
21
|
+
[string[]]$ExtraArgs
|
|
14
22
|
)
|
|
15
23
|
|
|
24
|
+
$ErrorActionPreference = 'Stop'
|
|
16
25
|
$PKG = 'dsh-ssh-tunnel'
|
|
17
26
|
$PLUGIN_ID = 'ssh-tunnel'
|
|
18
27
|
$GITHUB_REPO = if ($env:GITHUB_REPO) { $env:GITHUB_REPO } else { 'OMSociety/dsh-ssh-tunnel' }
|
|
19
28
|
$REGISTRY = if ($env:REGISTRY) { $env:REGISTRY } else { 'https://registry.npmjs.org' }
|
|
20
29
|
|
|
21
|
-
if ($
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
|
|
31
|
+
$LibDir = Join-Path $ScriptDir 'lib'
|
|
32
|
+
$WsExcludeCjs = Join-Path $LibDir 'ws-exclude.cjs'
|
|
33
|
+
$BundleCheckCjs = Join-Path $LibDir 'bundle-check.cjs'
|
|
34
|
+
$StripMountCjs = Join-Path $LibDir 'strip-mount.cjs'
|
|
35
|
+
|
|
36
|
+
function Show-Help {
|
|
37
|
+
@'
|
|
38
|
+
dsh-ssh-tunnel one-click installer
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install.ps1 -Profile <name> `
|
|
42
|
+
[-Version <ver>] [-From github|npm] [-FixProfile] [-Restart] [-DryRun] [-h]
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
-Profile <name> target profile name (required, no default)
|
|
45
|
+
-Version <ver> npm version or github branch/tag/commit (github default: HEAD)
|
|
46
|
+
-From github (default) | npm
|
|
47
|
+
-FixProfile optional: append minimumReleaseAgeExclude entry and strip the
|
|
48
|
+
legacy manual mount block (idempotent, write-back verified;
|
|
49
|
+
rolls back and exits non-zero on failure)
|
|
50
|
+
-Restart try pm2 restart dsh-web afterwards
|
|
51
|
+
-DryRun print the plan only, write nothing
|
|
52
|
+
-h show this help
|
|
53
|
+
|
|
54
|
+
Env: DSH_HOME (default %USERPROFILE%\.dsh), DSH_CMD (default dsh), REGISTRY,
|
|
55
|
+
GITHUB_REPO, DSH_INSTALL_YES=1 (skip the npx fallback confirmation)
|
|
56
|
+
'@
|
|
57
|
+
}
|
|
28
58
|
|
|
29
59
|
function Say([string]$m) { Write-Host "[install] $m" -ForegroundColor Green }
|
|
30
60
|
function Warn([string]$m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
|
|
31
|
-
function Die([string]$m)
|
|
61
|
+
function Die([string]$m, [int]$code = 1) { Write-Host "[error] $m" -ForegroundColor Red; exit $code }
|
|
32
62
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
return
|
|
63
|
+
function Show-Profiles {
|
|
64
|
+
$dir = Join-Path $DSH_HOME 'profiles'
|
|
65
|
+
if (-not (Test-Path -LiteralPath $dir -PathType Container)) { Write-Host " (profiles dir not found: $dir)"; return }
|
|
66
|
+
$items = @(Get-ChildItem -LiteralPath $dir -Directory -ErrorAction SilentlyContinue)
|
|
67
|
+
if ($items.Count -eq 0) { Write-Host " (no profile under $dir)"; return }
|
|
68
|
+
foreach ($p in $items) { Write-Host " - $($p.Name)" }
|
|
38
69
|
}
|
|
39
70
|
|
|
71
|
+
# ---------- argument triage ----------
|
|
72
|
+
# PowerShell -File mode passes unrecognized tokens (even --help / --dry-run)
|
|
73
|
+
# to the ValueFromRemainingArguments slot; route help requests out first.
|
|
74
|
+
$helpTokenRe = '^(-{1,2}h|-{1,2}help|h|help|/\?)$'
|
|
75
|
+
if ($H -or ($Version -match $helpTokenRe)) { Show-Help; exit 0 }
|
|
76
|
+
if ($ExtraArgs) {
|
|
77
|
+
foreach ($a in $ExtraArgs) { if ($a -match $helpTokenRe) { Show-Help; exit 0 } }
|
|
78
|
+
foreach ($a in $ExtraArgs) { Die "unknown argument: $a (use -h for usage)" 2 }
|
|
79
|
+
}
|
|
80
|
+
if ($Version -like '-*') { Die "unknown argument: $Version (use -h for usage; note PowerShell switches use a single dash, e.g. -DryRun)" 2 }
|
|
81
|
+
|
|
82
|
+
if ($env:DSH_HOME) { $DSH_HOME = $env:DSH_HOME }
|
|
83
|
+
elseif ($env:USERPROFILE) { $DSH_HOME = Join-Path $env:USERPROFILE '.dsh' }
|
|
84
|
+
else { $DSH_HOME = Join-Path $HOME '.dsh' }
|
|
85
|
+
|
|
86
|
+
# -Profile is required (validated manually so it never prompts interactively)
|
|
87
|
+
if ([string]::IsNullOrWhiteSpace($Profile)) {
|
|
88
|
+
Write-Host "[error] -Profile is required. Existing profiles under $DSH_HOME\profiles:" -ForegroundColor Red
|
|
89
|
+
Show-Profiles
|
|
90
|
+
exit 2
|
|
91
|
+
}
|
|
92
|
+
$ProfilesDir = Join-Path $DSH_HOME 'profiles'
|
|
93
|
+
$ProfileDir = Join-Path $ProfilesDir $Profile
|
|
94
|
+
$WsYml = Join-Path $ProfileDir 'pnpm-workspace.yaml'
|
|
95
|
+
$PatchYml = Join-Path $ProfileDir 'cordis.patch.yml'
|
|
96
|
+
$PkgJson = Join-Path $ProfileDir 'package.json'
|
|
97
|
+
|
|
98
|
+
if (-not (Test-Path -LiteralPath $ProfileDir -PathType Container)) {
|
|
99
|
+
Write-Host "[error] profile not found: $Profile ($ProfileDir). Existing profiles:" -ForegroundColor Red
|
|
100
|
+
Show-Profiles
|
|
101
|
+
exit 2
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ---------- prerequisites ----------
|
|
105
|
+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Die 'Node.js not found (need >= 20).' }
|
|
106
|
+
& node -p "process.versions.node.split('.')[0]" | Out-Null
|
|
107
|
+
if ($LASTEXITCODE -ne 0) { Die 'failed to query the node version.' }
|
|
108
|
+
$NodeMajor = [int](& node -p "process.versions.node.split('.')[0]")
|
|
109
|
+
if ($LASTEXITCODE -ne 0) { Die 'failed to query the node version.' }
|
|
110
|
+
if ($NodeMajor -lt 20) { Die "Node.js is too old (need >= 20)." }
|
|
111
|
+
|
|
40
112
|
function Resolve-AddSpec {
|
|
41
113
|
if ($From -eq 'npm') {
|
|
42
|
-
$given =
|
|
114
|
+
$given = 'latest'
|
|
115
|
+
if (-not [string]::IsNullOrWhiteSpace($Version)) { $given = $Version }
|
|
43
116
|
if ($given -eq 'latest') {
|
|
44
117
|
foreach ($tool in @('npm', 'pnpm')) {
|
|
45
118
|
if (Get-Command $tool -ErrorAction SilentlyContinue) {
|
|
46
|
-
$v =
|
|
47
|
-
if ($v) { return "$PKG@$([string]$v.Trim())" }
|
|
119
|
+
$v = & $tool view $PKG version "--registry=$REGISTRY" 2>$null | Select-Object -Last 1
|
|
120
|
+
if ($LASTEXITCODE -eq 0 -and $v) { return "$PKG@$([string]$v.Trim())" }
|
|
48
121
|
}
|
|
49
122
|
}
|
|
50
123
|
return "$PKG@latest"
|
|
@@ -54,95 +127,120 @@ function Resolve-AddSpec {
|
|
|
54
127
|
if ([string]::IsNullOrWhiteSpace($Version)) { return "$PKG@github:$GITHUB_REPO" }
|
|
55
128
|
return "$PKG@github:$GITHUB_REPO#$Version"
|
|
56
129
|
}
|
|
130
|
+
$AddSpec = Resolve-AddSpec
|
|
57
131
|
|
|
58
|
-
|
|
59
|
-
$
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
132
|
+
# ---------- dsh CLI resolution (npx fallback needs confirmation) ----------
|
|
133
|
+
$CliExe = ''
|
|
134
|
+
$CliArgs = @()
|
|
135
|
+
$NpxFallback = $false
|
|
136
|
+
if ($env:DSH_CMD) {
|
|
137
|
+
$parts = @($env:DSH_CMD -split '\s+' | Where-Object { $_ -ne '' })
|
|
138
|
+
$CliExe = $parts[0]
|
|
139
|
+
if ($parts.Count -gt 1) { $CliArgs = @($parts | Select-Object -Skip 1) }
|
|
140
|
+
}
|
|
141
|
+
elseif (Get-Command dsh -ErrorAction SilentlyContinue) {
|
|
142
|
+
$CliExe = 'dsh'
|
|
143
|
+
}
|
|
144
|
+
elseif (Get-Command npx -ErrorAction SilentlyContinue) {
|
|
145
|
+
$CliExe = 'npx'
|
|
146
|
+
$CliArgs = @('-y', '--package', '@deepseek-ai/dsh', 'dsh')
|
|
147
|
+
$NpxFallback = $true
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
Die 'dsh/npx not found. Install DSH or set DSH_CMD.'
|
|
151
|
+
}
|
|
63
152
|
|
|
64
|
-
$
|
|
65
|
-
$
|
|
66
|
-
if (-not $CLI) { Die 'dsh/npx not found. Install DSH or set DSH_CMD.' }
|
|
153
|
+
$CliDisplay = $CliExe
|
|
154
|
+
if ($CliArgs.Count -gt 0) { $CliDisplay = "$CliExe $($CliArgs -join ' ')" }
|
|
67
155
|
|
|
68
|
-
Say "Target: $
|
|
156
|
+
Say "Target: $CliDisplay plugin --profile $Profile add $AddSpec (profile dir: $ProfileDir)"
|
|
69
157
|
Say "Source: $From ($GITHUB_REPO)"
|
|
70
158
|
|
|
71
159
|
if ($DryRun) {
|
|
72
|
-
Say "[dry-run]
|
|
160
|
+
Say "[dry-run] 1) minimumReleaseAgeExclude handling (written only with -FixProfile, else warn only): $WsYml"
|
|
161
|
+
Say "[dry-run] 2) $CliDisplay plugin --profile $Profile add $AddSpec"
|
|
162
|
+
Say "[dry-run] 3) verify dsh.profile.bundles contains ${PKG}: $PkgJson"
|
|
163
|
+
Say "[dry-run] 4) allowBuilds precheck (advisory): $(Join-Path $ProfileDir 'node_modules\.modules.yaml')"
|
|
164
|
+
Say "[dry-run] 5) legacy manual mount strip (only with -FixProfile): $PatchYml"
|
|
165
|
+
if ($Restart) { Say '[dry-run] 6) pm2 restart dsh-web' }
|
|
166
|
+
else { Say '[dry-run] 6) hint to restart DSH web and hard-refresh the browser' }
|
|
73
167
|
exit 0
|
|
74
168
|
}
|
|
75
169
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!new RegExp("^\\s*-\\s+" + pkg.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&") + "\\s*$", "m").test(t)) {
|
|
83
|
-
if (/^\s*minimumReleaseAgeExclude:\s*$/m.test(t)) {
|
|
84
|
-
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - " + pkg);
|
|
85
|
-
} else {
|
|
86
|
-
t += "\nminimumReleaseAgeExclude:\n - " + pkg + "\n";
|
|
170
|
+
# npx fallback: confirm before any write happens
|
|
171
|
+
if ($NpxFallback) {
|
|
172
|
+
Say "dsh not found; will run via npx: $CliDisplay plugin --profile $Profile add $AddSpec"
|
|
173
|
+
if ($env:DSH_INSTALL_YES -ne '1') {
|
|
174
|
+
$answer = Read-Host 'This downloads and runs @deepseek-ai/dsh. Continue? [y/N]'
|
|
175
|
+
if ($answer -notmatch '^(y|Y)(es|ES)?$') { Die 'Aborted. Nothing was installed.' 1 }
|
|
87
176
|
}
|
|
88
177
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
$
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if ($
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
$
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
process.exit(bundles.includes(process.argv[2]) ? 0 : 1);
|
|
108
|
-
'@
|
|
109
|
-
& node -e $check (Join-Path $PROFILE_DIR 'package.json') $PKG
|
|
110
|
-
if ($LASTEXITCODE -ne 0) { Die "$PKG missing from dsh.profile.bundles" }
|
|
111
|
-
Say "bundle registered: $PKG"
|
|
112
|
-
|
|
113
|
-
if (Test-Path $PATCH_YML) {
|
|
114
|
-
$strip = @'
|
|
115
|
-
const fs = require("fs");
|
|
116
|
-
const p = process.argv[1];
|
|
117
|
-
const id = process.argv[2];
|
|
118
|
-
const lines = fs.readFileSync(p, "utf8").split("\n");
|
|
119
|
-
const out = [];
|
|
120
|
-
let i = 0, removed = false;
|
|
121
|
-
const idRe = new RegExp("id:\\s*" + id.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&") + "\\b");
|
|
122
|
-
while (i < lines.length) {
|
|
123
|
-
const line = lines[i];
|
|
124
|
-
if (/^[ \t]*- insert:\s*$/.test(line)) {
|
|
125
|
-
const block = [line];
|
|
126
|
-
let j = i + 1;
|
|
127
|
-
while (j < lines.length && lines[j].trim() !== "" && !/^-\s/.test(lines[j])) { block.push(lines[j]); j++; }
|
|
128
|
-
if (block.some((l) => idRe.test(l))) {
|
|
129
|
-
while (out.length && /^[ \t]*#/.test(out[out.length - 1])) out.pop();
|
|
130
|
-
i = j; removed = true; continue;
|
|
178
|
+
|
|
179
|
+
if (-not (Test-Path -LiteralPath $PkgJson)) { Die "Missing $PkgJson (profile not initialized?)" }
|
|
180
|
+
if ($FixProfile -and -not (Test-Path -LiteralPath $WsYml)) {
|
|
181
|
+
Die "Missing $WsYml (-FixProfile needs it to append minimumReleaseAgeExclude)"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# ---------- 1) minimumReleaseAgeExclude ----------
|
|
185
|
+
if ($FixProfile) {
|
|
186
|
+
$wsResult = & node $WsExcludeCjs $WsYml $PKG
|
|
187
|
+
if ($LASTEXITCODE -ne 0) { Die 'FixProfile: failed to update minimumReleaseAgeExclude (file left unchanged).' }
|
|
188
|
+
if ("$wsResult" -eq 'updated') { Say "Updated ${WsYml}: minimumReleaseAgeExclude += $PKG" }
|
|
189
|
+
else { Say "minimumReleaseAgeExclude already contains $PKG; skipped" }
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
if (Test-Path -LiteralPath $WsYml) {
|
|
193
|
+
& node $WsExcludeCjs --check $WsYml $PKG | Out-Null
|
|
194
|
+
if ($LASTEXITCODE -ne 0) {
|
|
195
|
+
Warn "minimumReleaseAgeExclude does not contain $PKG; pnpm minimumReleaseAge may block the install. Use -FixProfile to append it."
|
|
131
196
|
}
|
|
132
197
|
}
|
|
133
|
-
|
|
198
|
+
else {
|
|
199
|
+
Warn "pnpm-workspace.yaml not found; skipping the minimumReleaseAgeExclude check."
|
|
200
|
+
}
|
|
134
201
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
|
|
203
|
+
# ---------- 2) plugin add ----------
|
|
204
|
+
Say "Running: $CliDisplay plugin --profile $Profile add $AddSpec"
|
|
205
|
+
& $CliExe @CliArgs plugin --profile $Profile add $AddSpec
|
|
206
|
+
if ($LASTEXITCODE -ne 0) {
|
|
207
|
+
Warn 'dsh plugin add failed. Check network/registry, or run the command above manually.'
|
|
208
|
+
Warn "Prerequisite suggestion: dsh plugin --profile $Profile add dsh-better-sidebar"
|
|
209
|
+
exit 1
|
|
140
210
|
}
|
|
141
211
|
|
|
142
|
-
|
|
212
|
+
# ---------- 3) bundle check ----------
|
|
213
|
+
& node $BundleCheckCjs $PkgJson $PKG
|
|
214
|
+
if ($LASTEXITCODE -ne 0) { Die "$PKG missing from dsh.profile.bundles; mount not registered." }
|
|
215
|
+
Say "bundle registered: dsh.profile.bundles contains $PKG"
|
|
216
|
+
|
|
217
|
+
# ---------- 4) allowBuilds precheck (advisory) ----------
|
|
218
|
+
& node $BundleCheckCjs --ignored-builds $ProfileDir
|
|
219
|
+
if ($LASTEXITCODE -ne 0) { Warn 'allowBuilds precheck failed (install result unaffected).' }
|
|
220
|
+
|
|
221
|
+
# ---------- 5) legacy manual mount strip (only with -FixProfile) ----------
|
|
222
|
+
if ($FixProfile -and (Test-Path -LiteralPath $PatchYml)) {
|
|
223
|
+
$stripResult = & node $StripMountCjs $PatchYml $PLUGIN_ID
|
|
224
|
+
if ($LASTEXITCODE -ne 0) {
|
|
225
|
+
Die "FixProfile: failed to strip the legacy manual mount ($PatchYml rolled back to its original content; the plugin itself is installed)."
|
|
226
|
+
}
|
|
227
|
+
if ("$stripResult" -eq 'removed') { Say "Removed legacy manual $PLUGIN_ID mount from $PatchYml" }
|
|
228
|
+
else { Say 'No legacy manual mount found; skipped' }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Say "Done: $AddSpec (profile: $Profile)"
|
|
232
|
+
Say "Verify: dsh --profile $Profile --dump-config | Select-String '$PLUGIN_ID|$PKG'"
|
|
233
|
+
|
|
143
234
|
if ($Restart) {
|
|
144
|
-
if (Get-Command pm2 -ErrorAction SilentlyContinue) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
235
|
+
if (Get-Command pm2 -ErrorAction SilentlyContinue) {
|
|
236
|
+
Say 'Restarting dsh-web (pm2)...'
|
|
237
|
+
& pm2 restart dsh-web
|
|
238
|
+
if ($LASTEXITCODE -ne 0) { Warn 'pm2 restart failed; restart DSH manually.' }
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
Warn 'pm2 not found; restart DSH web manually.'
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
Say 'Next: restart DSH web and hard-refresh the browser (Ctrl/Cmd+Shift+R).'
|
|
148
246
|
}
|