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,50 +1,123 @@
1
1
  # =============================================================================
2
- # dsh-ssh-tunnel one-click install (Windows PowerShell 5.1+ / pwsh)
2
+ # dsh-ssh-tunnel one-click installer (Windows PowerShell 5.1+ and pwsh 7+)
3
3
  #
4
4
  # Usage:
5
- # irm https://raw.githubusercontent.com/OMSociety/dsh-ssh-tunnel/main/scripts/install.ps1 | iex
6
- # & ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/OMSociety/dsh-ssh-tunnel/main/scripts/install.ps1'))) -From github -Restart
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 ($env:DSH_HOME) { $DSH_HOME = $env:DSH_HOME }
22
- elseif ($env:USERPROFILE) { $DSH_HOME = Join-Path $env:USERPROFILE '.dsh' }
23
- else { $DSH_HOME = Join-Path $HOME '.dsh' }
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
- $PROFILE_DIR = Join-Path $DSH_HOME 'profiles\web'
26
- $WS_YML = Join-Path $PROFILE_DIR 'pnpm-workspace.yaml'
27
- $PATCH_YML = Join-Path $PROFILE_DIR 'cordis.patch.yml'
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) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 }
61
+ function Die([string]$m, [int]$code = 1) { Write-Host "[error] $m" -ForegroundColor Red; exit $code }
32
62
 
33
- function Get-DshCli {
34
- if ($env:DSH_CMD) { return $env:DSH_CMD }
35
- if (Get-Command dsh -ErrorAction SilentlyContinue) { return 'dsh' }
36
- if (Get-Command npx -ErrorAction SilentlyContinue) { return 'npx' }
37
- return $null
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 = if ([string]::IsNullOrWhiteSpace($Version)) { 'latest' } else { $Version }
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 = (& $tool view $PKG version "--registry=$REGISTRY" 2>$null | Select-Object -Last 1)
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
- if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Die 'Node.js not found (need >= 20).' }
59
- $NodeMajor = [int](& node -p "process.versions.node.split('.')[0]")
60
- if ($NodeMajor -lt 20) { Die "Node.js $(& node -v) is too old (need >= 20)." }
61
- if (-not (Test-Path $PROFILE_DIR)) { Die "Profile dir missing: $PROFILE_DIR" }
62
- if (-not (Test-Path $WS_YML)) { Die "Missing $WS_YML" }
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
- $ADD_SPEC = Resolve-AddSpec
65
- $CLI = Get-DshCli
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: $CLI plugin --profile web add $ADD_SPEC"
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] exclude $PKG from minimumReleaseAge; add $ADD_SPEC; verify bundles; strip manual $PLUGIN_ID mount"
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
- $wsScript = @'
77
- const fs = require("fs");
78
- const p = process.argv[1];
79
- const pkg = process.argv[2];
80
- let t = fs.readFileSync(p, "utf8");
81
- const before = t;
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
- if (t !== before) fs.writeFileSync(p, t);
90
- console.log(t === before ? "unchanged" : "updated");
91
- '@
92
- $wsResult = & node -e $wsScript $WS_YML $PKG
93
- if ($wsResult -eq 'updated') { Say "Updated $WS_YML minimumReleaseAgeExclude" } else { Say 'workspace ready' }
94
-
95
- Say "Running plugin add..."
96
- if ($CLI -eq 'npx') {
97
- & npx -y --package @deepseek-ai/dsh dsh plugin --profile web add $ADD_SPEC
98
- } else {
99
- & $CLI plugin --profile web add $ADD_SPEC
100
- }
101
- if ($LASTEXITCODE -ne 0) { Die 'dsh plugin add failed' }
102
-
103
- $check = @'
104
- const fs = require("fs");
105
- const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
106
- const bundles = p.dsh?.profile?.bundles ?? [];
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
- out.push(line); i++;
198
+ else {
199
+ Warn "pnpm-workspace.yaml not found; skipping the minimumReleaseAgeExclude check."
200
+ }
134
201
  }
135
- if (!removed) console.log("none");
136
- else { fs.writeFileSync(p, out.join("\n").replace(/\n{3,}/g, "\n\n")); console.log("removed"); }
137
- '@
138
- $mr = & node -e $strip $PATCH_YML $PLUGIN_ID
139
- if ($mr -eq 'removed') { Say "Removed manual $PLUGIN_ID mount from cordis.patch.yml" }
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
- Say "Done: $ADD_SPEC"
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) { pm2 restart dsh-web }
145
- else { Warn 'pm2 not found; restart DSH web manually' }
146
- } else {
147
- Say 'Next: restart DSH web and hard-refresh the browser.'
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
  }