diff --git a/scripts/install.ps1 b/scripts/install.ps1 index dfc6108..957a5f6 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,28 +1,385 @@ #Requires -Version 5.1 -# Bootstrap for dsh-telegram-channel manager. -# Safe for: irm .../install.ps1 | iex -# (top-level param()/CmdletBinding cannot be used with Invoke-Expression) +# Safe for: irm .../install.ps1 | iex +# Also: powershell -File install.ps1 -Action start +& { +#Requires -Version 5.1 +<# +.SYNOPSIS + dsh-telegram-channel manager menu: install / start / stop / status. +.EXAMPLE + irm https://raw.githubusercontent.com/hi-wenw/dsh-telegram-channel/master/scripts/install.ps1 | iex + +.EXAMPLE + .\scripts\install.ps1 -Action start + .\scripts\install.ps1 -Action install -Token '...' -UserId '123' +#> +[CmdletBinding()] +param( + [ValidateSet('', 'menu', 'install', 'start', 'stop', 'status', 'open')] + [string] $Action = '', + [string] $Token = $env:DSH_TELEGRAM_TOKEN, + [string] $UserId = $env:DSH_TELEGRAM_ALLOWED_USER_IDS, + [string] $ProfileName = 'web', + [string] $Source = 'github:hi-wenw/dsh-telegram-channel', + [int] $Port = 3080, + [switch] $Local, + [switch] $NoPersist +) + +Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -$manageUrl = 'https://raw.githubusercontent.com/hi-wenw/dsh-telegram-channel/master/scripts/manage.ps1' -$tmp = Join-Path $env:TEMP 'dsh-telegram-channel-manage.ps1' +function Write-Step([string] $Message) { + Write-Host "" + Write-Host ("==> " + $Message) -ForegroundColor Cyan +} -$local = $null -if ($PSScriptRoot) { - $candidate = Join-Path $PSScriptRoot 'manage.ps1' - if (Test-Path -LiteralPath $candidate) { - $local = $candidate +function Write-Ok([string] $Message) { + Write-Host $Message -ForegroundColor Green +} + +function Write-Warn2([string] $Message) { + Write-Host $Message -ForegroundColor Yellow +} + +function Ensure-Command([string] $Name) { + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + throw ("Command not found: " + $Name + ". Install DeepSeek Harness CLI (dsh) and add it to PATH.") } } -if ($local) { - Write-Host ("Running local manager: " + $local) -ForegroundColor Cyan - & $local @args - exit $LASTEXITCODE +function Get-DshHome { + if ($env:DSH_HOME) { return $env:DSH_HOME } + return (Join-Path $env:USERPROFILE '.dsh') } -Write-Host 'Downloading manager script...' -ForegroundColor Cyan -Invoke-WebRequest -UseBasicParsing -Uri $manageUrl -OutFile $tmp -& $tmp @args -exit $LASTEXITCODE +function Get-ProfileDir { + $dir = Join-Path (Get-DshHome) ('profiles\' + $ProfileName) + if (-not (Test-Path -LiteralPath $dir)) { + throw ("Profile directory not found: " + $dir + "`nRun dsh web once first.") + } + return $dir +} + +function Set-UserEnv([string] $Name, [string] $Value) { + Set-Item -Path ("Env:" + $Name) -Value $Value + if (-not $NoPersist) { + [Environment]::SetEnvironmentVariable($Name, $Value, 'User') + } +} + +function Get-UserEnv([string] $Name) { + $v = [Environment]::GetEnvironmentVariable($Name, 'User') + if ($v) { return $v } + return [Environment]::GetEnvironmentVariable($Name, 'Process') +} + +function Ensure-AllowBuilds([string] $ProfileDir) { + $path = Join-Path $ProfileDir 'pnpm-workspace.yaml' + $blockLines = @( + 'allowBuilds:' + ' dsh-telegram-channel: true' + ) + $block = $blockLines -join "`r`n" + if (-not (Test-Path -LiteralPath $path)) { + $content = @( + 'packages:' + ' - .' + '' + 'nodeLinker: hoisted' + 'autoInstallPeers: false' + '' + $block + ) -join "`r`n" + Set-Content -LiteralPath $path -Value $content -Encoding utf8 + Write-Host ("Created " + $path + " (allowBuilds)") + return + } + + $raw = Get-Content -LiteralPath $path -Raw + if ($raw -match '(?m)^\s*dsh-telegram-channel\s*:\s*true\s*$') { + Write-Host 'allowBuilds already present, skip' + return + } + + if ($raw -match '(?m)^allowBuilds\s*:') { + if ($raw -notmatch '(?m)^allowBuilds\s*:[\s\S]*?dsh-telegram-channel\s*:') { + $raw = $raw -replace '(?m)^(allowBuilds\s*:)', ('$1' + "`r`n dsh-telegram-channel: true") + $out = ($raw.TrimEnd() + "`r`n") + Set-Content -LiteralPath $path -Value $out -Encoding utf8 + Write-Host ("Wrote allowBuilds.dsh-telegram-channel -> " + $path) + } + return + } + + Add-Content -LiteralPath $path -Value ("`r`n" + $block + "`r`n") -Encoding utf8 + Write-Host ("Appended allowBuilds -> " + $path) +} + +function Get-ListenPids([int] $ListenPort) { + $pids = @() + try { + $pids = @( + Get-NetTCPConnection -LocalPort $ListenPort -State Listen -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty OwningProcess -Unique + ) + } catch { + $pids = @() + } + if ($pids.Count -eq 0) { + $lines = netstat -ano | Select-String -Pattern 'LISTENING' | Select-String -Pattern (':' + $ListenPort + '\s') + foreach ($line in $lines) { + if ($line -match '\s(\d+)\s*$') { + $pids += [int]$Matches[1] + } + } + $pids = @($pids | Select-Object -Unique) + } + return $pids +} + +function Get-DshWebPids { + $found = @() + Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | ForEach-Object { + $cmd = [string]$_.CommandLine + if ($cmd -match 'bin\.js' -and $cmd -match '\bweb\b' -and $cmd -match 'deepseek-ai') { + $found += [int]$_.ProcessId + } elseif ($cmd -match 'dsh\\lib\\bin\.js' -and $cmd -match '\bweb\b') { + $found += [int]$_.ProcessId + } + } + return @($found | Select-Object -Unique) +} + +function Show-Status { + Write-Step ('Status (profile=' + $ProfileName + ', port=' + $Port + ')') + + $token = Get-UserEnv 'DSH_TELEGRAM_TOKEN' + $allow = Get-UserEnv 'DSH_TELEGRAM_ALLOWED_USER_IDS' + if ($token) { Write-Ok ('Token: set (length ' + $token.Length + ')') } else { Write-Warn2 'Token: NOT set (DSH_TELEGRAM_TOKEN)' } + if ($allow) { Write-Ok ('Allowlist: ' + $allow) } else { Write-Warn2 'Allowlist: NOT set (DSH_TELEGRAM_ALLOWED_USER_IDS)' } + + $profileDir = $null + try { $profileDir = Get-ProfileDir } catch { Write-Warn2 $_.Exception.Message } + if ($profileDir) { + $pkg = Join-Path $profileDir 'package.json' + if (Test-Path -LiteralPath $pkg) { + $raw = Get-Content -LiteralPath $pkg -Raw + if ($raw -match 'dsh-telegram-channel') { + Write-Ok ('Plugin: found in ' + $pkg) + } else { + Write-Warn2 'Plugin: not in package.json yet (choose 1 to install)' + } + } + } + + $listen = @(Get-ListenPids $Port) + if ($listen.Count -gt 0) { + Write-Ok ('dsh web: RUNNING http://127.0.0.1:' + $Port + ' (PID ' + ($listen -join ', ') + ')') + } else { + Write-Warn2 ('dsh web: not listening on port ' + $Port + ' (choose 2 to start)') + } +} + +function Invoke-Install { + Ensure-Command 'dsh' + + $useToken = $Token + $useUserId = $UserId + if (-not $useToken) { $useToken = Get-UserEnv 'DSH_TELEGRAM_TOKEN' } + if (-not $useUserId) { $useUserId = Get-UserEnv 'DSH_TELEGRAM_ALLOWED_USER_IDS' } + + if (-not $useToken) { + $useToken = Read-Host 'Paste BotFather Bot Token' + } + $useToken = ([string]$useToken).Trim() + if (-not $useToken) { throw 'Missing Bot Token.' } + + if (-not $useUserId) { + $useUserId = Read-Host 'Paste Telegram numeric User ID (@userinfobot)' + } + $useUserId = ([string]$useUserId).Trim() + if ($useUserId -notmatch '^[0-9]+(,[0-9]+)*$') { + throw ("User ID must be numeric (comma-separated ok), got: '" + $useUserId + "'") + } + $useUserId = ($useUserId -replace '\s+', '') + + Write-Step 'Write user environment variables' + Set-UserEnv 'DSH_TELEGRAM_TOKEN' $useToken + Set-UserEnv 'DSH_TELEGRAM_ALLOWED_USER_IDS' $useUserId + Write-Host ('DSH_TELEGRAM_TOKEN = (set, length ' + $useToken.Length + ')') + Write-Host ('DSH_TELEGRAM_ALLOWED_USER_IDS = ' + $useUserId) + if ($NoPersist) { + Write-Warn2 '(-NoPersist: not written to user-level env)' + } + + $profileDir = Get-ProfileDir + Write-Step 'Ensure pnpm allowBuilds' + Ensure-AllowBuilds $profileDir + + $installSource = $Source + if ($Local) { + $scriptRoot = $PSScriptRoot + if (-not $scriptRoot -and $MyInvocation.MyCommand.Path) { + $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path + } + if (-not $scriptRoot) { + throw 'Cannot use -Local with remote irm|iex. git clone first, then run local script.' + } + $installSource = (Resolve-Path (Join-Path $scriptRoot '..')).Path + } + + Write-Step ('Install plugin: dsh plugin --profile ' + $ProfileName + ' add ' + $installSource) + & dsh plugin --profile $ProfileName add $installSource + if ($LASTEXITCODE -ne 0) { + throw ('dsh plugin add failed (exit=' + $LASTEXITCODE + '). Try: dsh plugin --profile ' + $ProfileName + ' remove dsh-telegram-channel') + } + + Write-Ok 'Install complete.' + Write-Host 'Next: choose 2 to start dsh web -> open a Web chat -> phone /sessions' +} + +function Invoke-Start { + Ensure-Command 'dsh' + $listen = @(Get-ListenPids $Port) + if ($listen.Count -gt 0) { + Write-Warn2 ('Already running: http://127.0.0.1:' + $Port + ' (PID ' + ($listen -join ', ') + ')') + Write-Host 'To restart: choose 3 (stop), then 2 (start).' + return + } + + $token = Get-UserEnv 'DSH_TELEGRAM_TOKEN' + $allow = Get-UserEnv 'DSH_TELEGRAM_ALLOWED_USER_IDS' + if (-not $token) { Write-Warn2 'Warning: DSH_TELEGRAM_TOKEN not set; bot may not poll.' } + if (-not $allow) { Write-Warn2 'Warning: DSH_TELEGRAM_ALLOWED_USER_IDS not set; all users denied.' } + + Write-Step 'Start dsh web in a new window (close that window to stop)' + $lines = @( + "`$env:DSH_TELEGRAM_TOKEN = [Environment]::GetEnvironmentVariable('DSH_TELEGRAM_TOKEN','User')" + "`$env:DSH_TELEGRAM_ALLOWED_USER_IDS = [Environment]::GetEnvironmentVariable('DSH_TELEGRAM_ALLOWED_USER_IDS','User')" + "if (-not `$env:DSH_TELEGRAM_TOKEN) { `$env:DSH_TELEGRAM_TOKEN = [Environment]::GetEnvironmentVariable('DSH_TELEGRAM_TOKEN','Process') }" + "if (-not `$env:DSH_TELEGRAM_ALLOWED_USER_IDS) { `$env:DSH_TELEGRAM_ALLOWED_USER_IDS = [Environment]::GetEnvironmentVariable('DSH_TELEGRAM_ALLOWED_USER_IDS','Process') }" + "Write-Host 'dsh-telegram-channel: starting dsh web ...' -ForegroundColor Cyan" + "Write-Host ('Token length: ' + (`$env:DSH_TELEGRAM_TOKEN + '').Length)" + "Write-Host ('Allow IDs: ' + `$env:DSH_TELEGRAM_ALLOWED_USER_IDS)" + "dsh web" + ) + $psCommand = $lines -join '; ' + Start-Process -FilePath 'powershell.exe' -ArgumentList @( + '-NoExit', + '-NoProfile', + '-ExecutionPolicy', 'Bypass', + '-Command', $psCommand + ) | Out-Null + + Write-Host 'New window opened. Waiting for port...' + $ok = $false + for ($i = 0; $i -lt 40; $i++) { + Start-Sleep -Milliseconds 500 + if ((Get-ListenPids $Port).Count -gt 0) { + $ok = $true + break + } + } + if ($ok) { + Write-Ok ('Started: http://127.0.0.1:' + $Port) + } else { + Write-Warn2 'Port not listening yet. Check the new window for errors.' + } +} + +function Invoke-Stop { + Write-Step ('Stop dsh web (port ' + $Port + ')') + $pids = @() + $pids += Get-ListenPids $Port + $pids += Get-DshWebPids + $pids = @($pids | Where-Object { $_ -gt 0 } | Select-Object -Unique) + + if ($pids.Count -eq 0) { + Write-Warn2 'No running dsh web found.' + return + } + + foreach ($procId in $pids) { + try { + $p = Get-Process -Id $procId -ErrorAction SilentlyContinue + $name = if ($p) { $p.ProcessName } else { '?' } + Write-Host ('Kill PID ' + $procId + ' (' + $name + ')') + Stop-Process -Id $procId -Force -ErrorAction Stop + } catch { + Write-Warn2 ('Cannot kill PID ' + $procId + ': ' + $_.Exception.Message) + } + } + + Start-Sleep -Seconds 1 + $left = @(Get-ListenPids $Port) + if ($left.Count -eq 0) { + Write-Ok 'Stopped.' + } else { + Write-Warn2 ('Port still in use: PID ' + ($left -join ', ')) + } +} + +function Invoke-Open { + $url = ('http://127.0.0.1:' + $Port) + $listen = @(Get-ListenPids $Port) + if ($listen.Count -eq 0) { + Write-Warn2 'Service not running. Choose 2 to start first.' + return + } + Start-Process $url + Write-Ok ('Opened ' + $url) +} + +function Show-Menu { + Write-Host '' + Write-Host '========================================' -ForegroundColor Green + Write-Host ' dsh-telegram-channel manager' -ForegroundColor Green + Write-Host '========================================' -ForegroundColor Green + Write-Host (' profile = ' + $ProfileName + ' port = ' + $Port) + Write-Host '' + Write-Host ' 1) Install / reinstall plugin (Token + allowlist)' + Write-Host ' 2) Start dsh web (new window)' + Write-Host ' 3) Stop dsh web' + Write-Host ' 4) Status' + Write-Host ' 5) Open browser' + Write-Host ' 0) Exit' + Write-Host '' +} + +function Invoke-MenuLoop { + while ($true) { + Show-Menu + $choice = Read-Host 'Enter number' + switch ($choice.Trim()) { + '1' { try { Invoke-Install } catch { Write-Host $_.Exception.Message -ForegroundColor Red } } + '2' { try { Invoke-Start } catch { Write-Host $_.Exception.Message -ForegroundColor Red } } + '3' { try { Invoke-Stop } catch { Write-Host $_.Exception.Message -ForegroundColor Red } } + '4' { try { Show-Status } catch { Write-Host $_.Exception.Message -ForegroundColor Red } } + '5' { try { Invoke-Open } catch { Write-Host $_.Exception.Message -ForegroundColor Red } } + '0' { Write-Host 'Bye.'; return } + '' { continue } + default { Write-Warn2 ('Invalid option: ' + $choice) } + } + } +} + +if (-not $Action) { + if ($PSBoundParameters.ContainsKey('Token') -or $PSBoundParameters.ContainsKey('UserId') -or $Local) { + $Action = 'install' + } else { + $Action = 'menu' + } +} + +switch ($Action) { + 'menu' { Invoke-MenuLoop } + 'install' { Invoke-Install } + 'start' { Invoke-Start } + 'stop' { Invoke-Stop } + 'status' { Show-Status } + 'open' { Invoke-Open } + default { throw ('Unknown Action: ' + $Action) } +} +} @args